From bb04392a81187ab39d7d344416d92b771aea48c6 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:35:26 +0800 Subject: [PATCH 1/5] Add exact official Brotli decoder backport candidate --- .../ubuntu-backports/brotli/brotli.lock.json | 100 ++++++++++++ .../brotli/build-brotli-backport.py | 143 ++++++++++++++++++ .../ubuntu-backports/brotli/roundtrip.c | 62 ++++++++ .../brotli/test_brotli_backport.py | 91 +++++++++++ 4 files changed, 396 insertions(+) create mode 100644 server/security/ubuntu-backports/brotli/brotli.lock.json create mode 100644 server/security/ubuntu-backports/brotli/build-brotli-backport.py create mode 100644 server/security/ubuntu-backports/brotli/roundtrip.c create mode 100644 server/security/ubuntu-backports/brotli/test_brotli_backport.py diff --git a/server/security/ubuntu-backports/brotli/brotli.lock.json b/server/security/ubuntu-backports/brotli/brotli.lock.json new file mode 100644 index 0000000000..c012752d77 --- /dev/null +++ b/server/security/ubuntu-backports/brotli/brotli.lock.json @@ -0,0 +1,100 @@ +{ + "schemaVersion": 1, + "sourcePackage": "brotli", + "ubuntuSourceVersion": "1.2.0-3build1", + "customVersion": "1.2.0-3build1+pasturestack.1", + "distribution": "resolute", + "osVersion": "26.04", + "architecture": "amd64", + "sourceDateEpoch": 1792886400, + "identity": "First-party rebuild with unchanged Ubuntu packaging and the exact Google security commit; not issued by Canonical.", + "runtimePackages": [ + "libbrotli1" + ], + "sourceFiles": [ + { + "name": "brotli_1.2.0-3build1.dsc", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/b/brotli/brotli_1.2.0-3build1.dsc", + "sha256": "07c51f4299d065d4ae3cc269e9a837b9723453632fe62039cc47723a7d620eda", + "size": 2281 + }, + { + "name": "brotli_1.2.0.orig.tar.gz", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/b/brotli/brotli_1.2.0.orig.tar.gz", + "sha256": "eb5f7dadf215d0670665fd81566e1fe2dfdc154d983f09142de7299df4c182e6", + "size": 646398 + }, + { + "name": "brotli_1.2.0-3build1.debian.tar.xz", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/b/brotli/brotli_1.2.0-3build1.debian.tar.xz", + "sha256": "7ebbb009f9e33f54a7790e95da60af4667a3fc4a914b79ad49bfc06bb396d829", + "size": 5976 + } + ], + "patches": [ + { + "name": "b418fa81c48262a86a95332b0bc6d62c30aa78b6.patch", + "url": "https://github.com/google/brotli/commit/b418fa81c48262a86a95332b0bc6d62c30aa78b6.patch", + "sha256": "a4516e1df041108aefba2b3be1faf4c907a8b051710f205dc55d8b8f9d208dec", + "size": 9821, + "commit": "b418fa81c48262a86a95332b0bc6d62c30aa78b6", + "fixedCves": [ + "CVE-2026-111497" + ], + "firstFixedVersion": "1.2.1" + } + ], + "baselineBinary": { + "name": "libbrotli1_1.2.0-3build1_amd64.deb", + "url": "https://archive.ubuntu.com/ubuntu/pool/main/b/brotli/libbrotli1_1.2.0-3build1_amd64.deb", + "sha256": "858242571fc2585c1f8eb83ae01d020f0f4a92679d0de8fe31146f6687bdd84a", + "size": 342830, + "version": "1.2.0-3build1" + }, + "baselineSha256": { + "c/dec/decode.c": "f7749b1296128c0add4eaa454b9d82ee1c7b0451c1c9ed47d96733e6de6f2d7b", + "c/include/brotli/decode.h": "027b91e62e85de6955e6334628b2ba0c77901a449e169fd9f95d6ea1cc91f980" + }, + "patchedSha256": { + "c/dec/decode.c": "6eaa6b923ab4dfa6c1ef4d517f19d4382c646c905d273baa1077b3fb4b27d586", + "c/include/brotli/decode.h": "5ef8d5603c9a7fce382cb2e8d0cf0c3bc2e8274f7601957ab671ba3b372937e7" + }, + "ubuntuPackagingSha256": { + "debian/brotli.install": "c0f06a1e597977eefcd8b2a5eb99e678659de7a3adc59a74365f34ea29af793a", + "debian/brotli.manpages": "b3185d1368c756cc6cb54aa062781bc1b6222c8857e226b6cbf4c4f189970e63", + "debian/control": "6c68968c8170775d018a9867abd2b16557c1388168b9b80a20239ffa2a3cb534", + "debian/copyright": "24a64e5bb83d0960d1835696a1e23c0896ad6055b0ca47c66ab0eb9a766324b1", + "debian/libbrotli-dev.install": "77b28dcc32564d5844d26e329dcfc133042bad34aea9e759433ec4fceea8a1b6", + "debian/libbrotli1.install": "65e99705c6908377687751449eaf8fba0157ff8330b0f2ca14261fae0c4fe3d3", + "debian/libbrotli1.lintian-overrides": "2348de77b1f367dbd9891dd844d22a63299861d50db72b621849ecd0466467f2", + "debian/libbrotli1.symbols": "b40f838b0bcad033c4fe8e78d2f8d003062f2bad53efa5716d7c5042c3c15432", + "debian/not-installed": "30dbe7c8c1ef2061364b7d9495bf2d3c058d1bc9038a146421052cebe38ef49c", + "debian/patches/0001-disable-BROTLI_MODEL-macro-for-some-targets.patch": "b4db797f22d7932a97d738596d0ba1ca3c1f201421243bed96d3888888ad8f75", + "debian/patches/series": "5674e4744078f82842b0059baf59d616bc8f1fb122a767c7b7b59fc3d43fe54a", + "debian/python3-brotli.lintian-overrides": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "debian/rename-static-libs.sh": "7422c83b1c378e1defc9996bc3d25800ae7fae4a09434baaefaae25f4d8a1536", + "debian/rules": "aca35475dbd2576e31a33528b8862fb04525d6175b506ebaf8a7a4bf26986546", + "debian/salsa-ci.yml": "32997347e7094680bbc5a1dec4ec4f04424b79bcf4ccbb6bb318f0b601e78e3f", + "debian/source/format": "1be7080d72e6b566df3e236ce2c55efdfbbb8fa1c972d825e5b672ff8773be1a", + "debian/tests/control": "caafcc857a57c341a2c24eab30e483185131d98fa9ca6162490bb718b85daf54", + "debian/tests/python3": "b722bcad10002340598980243e88234aeea192bb0a67d9f78f23b69a92044fd4", + "debian/upstream/metadata": "1913e3632e37a9470a125d5045d1b2a85d6464b36a4564135c291df038c30af5", + "debian/watch": "aaba817f66ca20727032e925eabe92462ab364c0004ae17ed290e4563279a6d4" + }, + "buildContract": { + "nativeUbuntuAmd64": true, + "buildUser": "non-root", + "originalRulesAndDependenciesPreserved": true, + "addedSkips": 0, + "runtimeInstalled": false, + "requiredSonames": [ + "libbrotlicommon.so.1", + "libbrotlidec.so.1", + "libbrotlienc.so.1" + ] + }, + "limitations": [ + "No exploit, CVE trigger or crafted invalid stream is executed. Remediation is established by exact official source-patch closure; ordinary compression/streaming and ABI controls establish compatibility.", + "Source archives are SHA256-pinned and matched by the signed fixed-snapshot index in native CI; no Canonical signature is claimed for first-party rebuilt binaries." + ] +} diff --git a/server/security/ubuntu-backports/brotli/build-brotli-backport.py b/server/security/ubuntu-backports/brotli/build-brotli-backport.py new file mode 100644 index 0000000000..130f6c6a5c --- /dev/null +++ b/server/security/ubuntu-backports/brotli/build-brotli-backport.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Exact official Brotli decoder fix with original Ubuntu package semantics.""" +import argparse +import importlib.util +import json +from pathlib import Path +import re +import shlex +import subprocess +import sys +import xml.etree.ElementTree as ET + +HERE = Path(__file__).resolve().parent +sys.dont_write_bytecode = True +SPEC = importlib.util.spec_from_file_location('brotli_source_helpers', HERE.parent / 'source_helpers.py') +H = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(H) +COMMIT = 'b418fa81c48262a86a95332b0bc6d62c30aa78b6' +VERSION = '1.2.0-3build1' +SONAMES = ['libbrotlicommon.so.1', 'libbrotlidec.so.1', 'libbrotlienc.so.1'] +BOUNDARY = {'c/dec/decode.c', 'c/include/brotli/decode.h'} + + +def load_lock(file): + lock = json.loads(file.read_text(encoding='utf-8')) + H.require(all(lock.get(k) == v for k, v in { + 'schemaVersion': 1, 'sourcePackage': 'brotli', 'ubuntuSourceVersion': VERSION, + 'customVersion': VERSION + '+pasturestack.1', 'distribution': 'resolute', + 'osVersion': '26.04', 'architecture': 'amd64', 'runtimePackages': ['libbrotli1']}.items()), + 'Unexpected Brotli package/platform boundary') + H.require(type(lock['sourceDateEpoch']) is int and lock['sourceDateEpoch'] > 0, 'Invalid source date') + H.require({p['name'] for p in lock['sourceFiles']} == { + 'brotli_1.2.0-3build1.dsc', 'brotli_1.2.0.orig.tar.gz', 'brotli_1.2.0-3build1.debian.tar.xz'} + and len(lock['sourceFiles']) == 3, 'Wrong Ubuntu source set') + H.require(len(lock['patches']) == 1 and lock['patches'][0]['commit'] == COMMIT + and lock['patches'][0]['fixedCves'] == ['CVE-2026-111497'], 'Wrong official security patch') + H.require(lock['baselineBinary']['name'] == 'libbrotli1_' + VERSION + '_amd64.deb' + and lock['baselineBinary']['version'] == VERSION, 'Wrong original Ubuntu binary') + for item in H.inputs(lock): + H.require(re.fullmatch(r'[A-Za-z0-9._~+-]+', item['name']) + and re.fullmatch('[0-9a-f]{64}', item['sha256']) + and type(item['size']) is int and 0 < item['size'] < 8 * 1024 * 1024, 'Unsafe/unpinned input') + url = ('https://github.com/google/brotli/commit/' + COMMIT + '.patch' if item in lock['patches'] + else 'https://archive.ubuntu.com/ubuntu/pool/main/b/brotli/' + item['name']) + H.require(item['url'] == url, 'Unexpected official source URL') + H.require(set(lock['baselineSha256']) == set(lock['patchedSha256']) == BOUNDARY, + 'Security patch source boundary changed') + for group in ['baselineSha256', 'patchedSha256', 'ubuntuPackagingSha256']: + for name, sha in lock[group].items(): + H.require(not Path(name).is_absolute() and '..' not in Path(name).parts + and '\\' not in name and re.fullmatch('[0-9a-f]{64}', sha), 'Unsafe/unpinned source path') + H.require(lock['buildContract']['requiredSonames'] == SONAMES, 'Wrong ABI contract') + return lock + + +def abi(dynamic, symbols, original_dynamic, original_symbols, soname): + sonames = lambda text: re.findall(r'SONAME.*\[(.+?)\]', text) + needed = lambda text: sorted(re.findall(r'NEEDED.*\[(.+?)\]', text)) + exports = lambda text: {line.split()[-1] for line in text.splitlines() if line.split()} + H.require(sonames(dynamic) == sonames(original_dynamic) == [soname], 'Brotli SONAME changed') + H.require(needed(dynamic) == needed(original_dynamic), 'Brotli runtime dependencies changed') + H.require(exports(symbols) == exports(original_symbols) and exports(symbols), 'Brotli exported ABI changed') + return {'soname': soname, 'needed': needed(dynamic), 'exports': len(exports(symbols)), 'status': 'PASS'} + + +def owning_results(xml, python_log): + root = ET.fromstring(xml) + cases = root.findall('.//testcase') + H.require(len(cases) >= 12 and not root.findall('.//failure') and not root.findall('.//error') + and not root.findall('.//skipped') + and all(c.get('status') in (None, 'run') for c in cases), 'Brotli CMake suite failed/skipped/empty') + counts = re.findall(r'^Ran ([0-9]+) tests? in ', python_log, re.M) + H.require(len(counts) == 1 and int(counts[0]) > 0 and re.search(r'^OK\s*$', python_log, re.M) + and not re.search(r'\b(?:skipped|FAILED|ERROR|FAIL)\b', python_log), 'Brotli Python suite failed/skipped/empty') + return {'status': 'PASS', 'cmakeCases': len(cases), 'pythonCases': int(counts[0]), 'addedSkips': 0} + + +def verify(lock, source, extracted, work, env, log): + caches = list(source.glob('obj-*/CMakeCache.txt')) + H.require(len(caches) == 1, 'Missing/ambiguous original package CMake build') + build = caches[0].parent + cmake_log, junit, python_log = work / 'cmake-tests.log', work / 'cmake-tests.xml', work / 'python-tests.log' + H.run(['ctest', '--test-dir', str(build), '--output-on-failure', '--no-tests=error', + '--output-junit', str(junit)], source, env, cmake_log) + modules = list(source.glob('.pybuild/*/build/brotli.py')) + H.require(len(modules) == 1, 'Missing/ambiguous original Python module build') + child = dict(env, PYTHONPATH=str(modules[0].parent)) + H.run(['python3', '-m', 'unittest', 'discover', '-s', 'python', '-p', '*_test.py', '-v'], source, child, python_log) + suites = owning_results(junit.read_text(), python_log.read_text()) + original = work / 'original-package' + H.run(['dpkg-deb', '--extract', str(work / 'inputs' / lock['baselineBinary']['name']), str(original)], work, env, log) + controls = [] + library_dir = Path('usr/lib/x86_64-linux-gnu') + for soname in SONAMES: + new, old = extracted / library_dir / soname, original / library_dir / soname + dynamic = H.capture(['readelf', '--dynamic', str(new)], work, env) + old_dynamic = H.capture(['readelf', '--dynamic', str(old)], work, env) + nm = ['nm', '--dynamic', '--defined-only', '--extern-only'] + controls.append(abi(dynamic, H.capture(nm + [str(new)], work, env), old_dynamic, + H.capture(nm + [str(old)], work, env), soname)) + H.require('GNU_RELRO' in H.capture(['readelf', '--program-headers', str(new)], work, env), 'RELRO regressed') + now = lambda text: 'BIND_NOW' in text or bool(re.search(r'FLAGS_1.*NOW', text)) + H.require(not now(old_dynamic) or now(dynamic), 'Original BIND_NOW hardening regressed') + probe = work / 'brotli-roundtrip' + cc = ['cc', '-std=c11', '-Wall', '-Wextra', '-Werror'] + for name in ['CPPFLAGS', 'CFLAGS', 'LDFLAGS']: + cc += shlex.split(H.capture(['dpkg-buildflags', '--get', name], work, env)) + libs = extracted / library_dir + H.run(cc + ['-I' + str(source / 'c/include'), str(HERE / 'roundtrip.c'), '-L' + str(libs), + '-Wl,-rpath,' + str(libs), '-l:libbrotlienc.so.1', '-l:libbrotlidec.so.1', + '-l:libbrotlicommon.so.1', '-o', str(probe)], work, env, log) + result = H.capture([str(probe)], work, dict(env, LD_LIBRARY_PATH=str(libs))) + H.require(result == 'PASS: 15 valid roundtrips, 45 streaming roundtrips, three linked SONAMEs', 'Packaged valid-input API control failed') + report = {'status': 'PASS', 'owningSuites': suites, 'abi': controls, 'packageApi': result, + 'runtimeInstalled': False, 'exploitExecuted': False, + 'remediationEvidence': 'Exact official two-file source patch and post-patch hashes; no malformed-stream trigger executed.'} + receipt = work / 'brotli-tests.json' + receipt.write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') + return report, [(p, p.name) for p in [cmake_log, junit, python_log, receipt]] + + +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(HERE / 'brotli.lock.json') + if args.validate_lock: + print('OFFICIAL_SOURCE_LOCK_OK brotli') + return + H.require(args.output_dir is not None, '--output-dir is required') + H.build(args, lock, HERE, verify, ['cc', 'ctest', 'dpkg-buildflags']) + + +if __name__ == '__main__': + try: + main() + except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error: + print('BROTLI_BACKPORT_FAILED: ' + str(error), file=sys.stderr) + sys.exit(1) diff --git a/server/security/ubuntu-backports/brotli/roundtrip.c b/server/security/ubuntu-backports/brotli/roundtrip.c new file mode 100644 index 0000000000..064c134466 --- /dev/null +++ b/server/security/ubuntu-backports/brotli/roundtrip.c @@ -0,0 +1,62 @@ +/* Ordinary valid-input API/streaming control; no malformed stream or CVE trigger. */ +#include +#include +#include +#include +#include + +static int check(const unsigned char *input, size_t size, int quality) { + size_t capacity = BrotliEncoderMaxCompressedSize(size); + unsigned char *compressed = malloc(capacity ? capacity : 1); + unsigned char *output = malloc(size + 1); + size_t compressed_size = capacity, output_size = size + 1; + if (!compressed || !output) return 1; + if (!BrotliEncoderCompress(quality, BROTLI_DEFAULT_WINDOW, BROTLI_MODE_GENERIC, + size, input, &compressed_size, compressed)) return 2; + if (BrotliDecoderDecompress(compressed_size, compressed, &output_size, output) + != BROTLI_DECODER_RESULT_SUCCESS || output_size != size || + memcmp(input, output, size)) return 3; + const size_t chunks[] = {1, 7, 64}; + for (size_t c = 0; c < sizeof(chunks) / sizeof(chunks[0]); ++c) { + BrotliDecoderState *state = BrotliDecoderCreateInstance(NULL, NULL, NULL); + size_t consumed = 0, produced = 0; + BrotliDecoderResult result = BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; + if (!state) return 4; + for (size_t iteration = 0; iteration < compressed_size + size + 16; ++iteration) { + size_t available_in = compressed_size - consumed; + if (available_in > chunks[c]) available_in = chunks[c]; + size_t available_out = size + 1 - produced; + if (available_out > chunks[c]) available_out = chunks[c]; + const unsigned char *next_in = compressed + consumed; + unsigned char *next_out = output + produced; + size_t in_before = available_in, out_before = available_out; + result = BrotliDecoderDecompressStream(state, &available_in, &next_in, + &available_out, &next_out, NULL); + consumed += in_before - available_in; + produced += out_before - available_out; + if (result == BROTLI_DECODER_RESULT_SUCCESS) break; + if (result == BROTLI_DECODER_RESULT_ERROR || + (in_before == available_in && out_before == available_out)) return 5; + } + if (result != BROTLI_DECODER_RESULT_SUCCESS || consumed != compressed_size || + produced != size || !BrotliDecoderIsFinished(state) || + memcmp(input, output, size)) return 6; + BrotliDecoderDestroyInstance(state); + } + free(compressed); + free(output); + return 0; +} + +int main(void) { + unsigned char repeated[8192]; + for (size_t n = 0; n < sizeof(repeated); ++n) repeated[n] = (unsigned char)(n % 251); + const unsigned char text[] = "PastureStack ordinary Brotli API and streaming control.\n"; + const int qualities[] = {0, 1, 6, 9, 11}; + for (size_t q = 0; q < sizeof(qualities) / sizeof(qualities[0]); ++q) { + if (check(text, 0, qualities[q]) || check(text, sizeof(text) - 1, qualities[q]) || + check(repeated, sizeof(repeated), qualities[q])) return 1; + } + puts("PASS: 15 valid roundtrips, 45 streaming roundtrips, three linked SONAMEs"); + return 0; +} diff --git a/server/security/ubuntu-backports/brotli/test_brotli_backport.py b/server/security/ubuntu-backports/brotli/test_brotli_backport.py new file mode 100644 index 0000000000..7f277fb30e --- /dev/null +++ b/server/security/ubuntu-backports/brotli/test_brotli_backport.py @@ -0,0 +1,91 @@ +"""Focused source/receipt guards; no local compilation, install or malformed data.""" +import copy +import importlib.util +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest import mock + +sys.dont_write_bytecode = True +HERE = Path(__file__).resolve().parent +SPEC = importlib.util.spec_from_file_location('brotli_backport', HERE / 'build-brotli-backport.py') +M = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(M) + + +class BrotliBackport(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='brotli-unit-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.lock = M.load_lock(HERE / 'brotli.lock.json') + + def altered(self, key, value): + lock = copy.deepcopy(self.lock); lock[key] = value + p = self.root / 'lock.json'; p.write_text(json.dumps(lock)) + return M.load_lock(p) + + def test_locked_official_boundary(self): + for k, v in [('architecture', 'arm64'), ('runtimePackages', ['libbrotli-dev']), + ('sourcePackage', 'other'), ('ubuntuSourceVersion', '1.2.1'), + ('patches', []), ('baselineSha256', {})]: + with self.subTest(key=k), self.assertRaises(ValueError): self.altered(k, v) + for key, value in [('url', 'https://example.invalid/source'), ('sha256', ''), ('name', '../outside')]: + entries = copy.deepcopy(self.lock['sourceFiles']); entries[0][key] = value + with self.subTest(key=key), self.assertRaises(ValueError): self.altered('sourceFiles', entries) + + def test_three_library_abi_contract_rejects_any_change(self): + dynamic = 'SONAME [libbrotlidec.so.1]\nNEEDED [libbrotlicommon.so.1]\nNEEDED [libc.so.6]' + symbols = '0 T BrotliDecoderDecompress\n1 W extra@VERSION_1' + self.assertEqual(M.abi(dynamic, symbols, dynamic, symbols, 'libbrotlidec.so.1')['status'], 'PASS') + for new, exports in [(dynamic.replace('so.1]', 'so.2]', 1), symbols), + (dynamic + '\nNEEDED [unknown.so.1]', symbols), + (dynamic.replace('\nNEEDED [libc.so.6]', ''), symbols), + (dynamic, symbols.replace('VERSION_1', 'VERSION_2')), + (dynamic, '0 T BrotliDecoderDecompress')]: + with self.assertRaises(ValueError): M.abi(new, exports, dynamic, symbols, 'libbrotlidec.so.1') + + def test_owning_receipt_requires_real_cases_and_no_failures_or_skips(self): + xml = '' + '' * 14 + '' + log = 'Ran 21 tests in 0.1s\n\nOK\n' + self.assertEqual(M.owning_results(xml, log)['cmakeCases'], 14) + for badxml, badlog in [(xml.replace('/>', '>', 1), log), + (xml.replace('/>', '>', 1), log), + ('', log), (xml, 'Ran 0 tests in 0.0s\nOK'), + (xml, log.replace('OK', 'OK (skipped=1)')), (xml, log.replace('OK', 'FAILED'))]: + with self.assertRaises(ValueError): M.owning_results(badxml, badlog) + + def test_common_build_reaches_verified_download_then_stops_before_build(self): + args = type('Args', (), {'output_dir': self.root / 'out', 'work_dir': self.root / 'work', + 'cache_dir': None, 'jobs': 2})() + real_read = Path.read_text + def read(path, *a, **kw): + return 'ID=ubuntu\nVERSION_ID="26.04"\n' if path.as_posix() == '/etc/os-release' else real_read(path, *a, **kw) + class DownloadReached(Exception): pass + with mock.patch.object(Path, 'read_text', read), mock.patch.object(M.H.os, 'geteuid', create=True, return_value=10001), \ + mock.patch.object(M.H.shutil, 'which', return_value='/usr/bin/tool'), \ + mock.patch.object(M.H, 'capture', return_value='amd64'), \ + mock.patch.object(M.H.COMMON, 'download', side_effect=DownloadReached) as download, \ + mock.patch.object(M.H, 'run') as run: + with self.assertRaises(DownloadReached): M.H.build(args, self.lock, HERE, M.verify, []) + self.assertEqual(download.call_args.args[0], self.lock['sourceFiles'][0]) + run.assert_not_called() + + def test_root_and_foreign_distribution_cannot_build(self): + for release, uid in [('ID=ubuntu\nVERSION_ID="26.04"\n', 0), ('ID=debian\nVERSION_ID="13"\n', 10001)]: + with mock.patch.object(Path, 'read_text', return_value=release), \ + mock.patch.object(M.H.os, 'geteuid', create=True, return_value=uid): + with self.assertRaisesRegex(ValueError, 'non-root'): M.H.build(None, self.lock, HERE, M.verify, []) + + def test_only_exact_official_two_file_patch_is_accepted(self): + for item in M.H.inputs(self.lock): + (self.root / item['name']).write_bytes(b'changed') + with self.assertRaises(ValueError): M.H.verify_inputs(self.lock, self.root) + # The real official patch and DSC are validated by source/native checks; + # this test proves poisoned input never reaches source preparation. + + +if __name__ == '__main__': + unittest.main() From def8a6ac6b55c9e8aac363b382b2afdd860aef88 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:35:49 +0800 Subject: [PATCH 2/5] Select Brotli in the existing isolated native workflow --- .github/workflows/ubuntu-backport-native.yml | 5 +++-- server/security/ubuntu-backports/Dockerfile.native-ci | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ubuntu-backport-native.yml b/.github/workflows/ubuntu-backport-native.yml index 4cc7b72937..edcac901c5 100644 --- a/.github/workflows/ubuntu-backport-native.yml +++ b/.github/workflows/ubuntu-backport-native.yml @@ -14,6 +14,7 @@ on: - 'server/security/ubuntu-backports/jq/**' - 'server/security/ubuntu-backports/p11-kit/**' - 'server/security/ubuntu-backports/libxrender/**' + - 'server/security/ubuntu-backports/brotli/**' - 'server/security/ubuntu-backports/source_helpers.py' workflow_dispatch: inputs: @@ -22,7 +23,7 @@ on: required: true type: choice default: all - options: [all, glibc, rust-coreutils, jq, p11-kit, libxrender] + options: [all, glibc, rust-coreutils, jq, p11-kit, libxrender, brotli] permissions: contents: read @@ -39,7 +40,7 @@ jobs: strategy: fail-fast: false matrix: - module: ${{ fromJSON(inputs.module && inputs.module != 'all' && format('["{0}"]', inputs.module) || '["glibc","rust-coreutils","jq","p11-kit","libxrender"]') }} + module: ${{ fromJSON(inputs.module && inputs.module != 'all' && format('["{0}"]', inputs.module) || '["glibc","rust-coreutils","jq","p11-kit","libxrender","brotli"]') }} env: MODULE: ${{ matrix.module }} SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/server/security/ubuntu-backports/Dockerfile.native-ci b/server/security/ubuntu-backports/Dockerfile.native-ci index 7dff462a67..db1e47e4ba 100644 --- a/server/security/ubuntu-backports/Dockerfile.native-ci +++ b/server/security/ubuntu-backports/Dockerfile.native-ci @@ -12,7 +12,7 @@ SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] 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 RUN <<'SH' set -euo pipefail -case "$MODULE" in glibc|rust-coreutils|jq|p11-kit|libxrender) ;; *) exit 2 ;; esac +case "$MODULE" in glibc|rust-coreutils|jq|p11-kit|libxrender|brotli) ;; *) exit 2 ;; esac mkdir -p /tmp/ca-bootstrap /etc/ssl/certs dpkg-deb --extract /tmp/ca-certificates.deb /tmp/ca-bootstrap while IFS= read -r certificate; do From 7f90a147c69ff1fdbfbf8a7e48b0bf12a95fbc56 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:39:59 +0800 Subject: [PATCH 3/5] Pin official LF post-patch Brotli source bytes --- server/security/ubuntu-backports/brotli/brotli.lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/security/ubuntu-backports/brotli/brotli.lock.json b/server/security/ubuntu-backports/brotli/brotli.lock.json index c012752d77..ae7874ed61 100644 --- a/server/security/ubuntu-backports/brotli/brotli.lock.json +++ b/server/security/ubuntu-backports/brotli/brotli.lock.json @@ -56,8 +56,8 @@ "c/include/brotli/decode.h": "027b91e62e85de6955e6334628b2ba0c77901a449e169fd9f95d6ea1cc91f980" }, "patchedSha256": { - "c/dec/decode.c": "6eaa6b923ab4dfa6c1ef4d517f19d4382c646c905d273baa1077b3fb4b27d586", - "c/include/brotli/decode.h": "5ef8d5603c9a7fce382cb2e8d0cf0c3bc2e8274f7601957ab671ba3b372937e7" + "c/dec/decode.c": "8d7beeafed5ff9f3f00f6e528e92ad6a36059b3549356bed5a9ca3ee16938438", + "c/include/brotli/decode.h": "e2f5fdc3ae41e6b7e8986538e421ae79c095541bacf32f2bbfccf8e056405444" }, "ubuntuPackagingSha256": { "debian/brotli.install": "c0f06a1e597977eefcd8b2a5eb99e678659de7a3adc59a74365f34ea29af793a", From f4dd6fbfb2dd5cceee7a94d66b5b085c8fb15974 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:58:05 +0800 Subject: [PATCH 4/5] Extend qualified security bundle to Brotli without rebuilding prior families --- .../publish-ubuntu-security-backports.yml | 6 +- server/security/ubuntu-backports/README.md | 9 +- .../ubuntu-backports/bundle/Dockerfile.bundle | 2 +- .../bundle/installer/README.md | 11 +- .../bundle/installer/brotli_test.go | 37 ++++++ .../installer/consumer-fixture.Dockerfile | 12 +- .../bundle/installer/consumer-smoke.sh | 11 +- .../bundle/installer/installer.go | 2 +- .../bundle/installer/installer_test.go | 2 +- .../bundle/installer/schema.go | 5 +- .../testdata/consumer-brotli-probe.c | 31 +++++ .../bundle/qualify-native-packages.py | 42 ++++--- .../bundle/source-inputs.lock.json | 110 ++++++++++++++---- .../bundle/test_qualify_native_packages.py | 21 +++- .../bundle/test_verify_consumer_evidence.py | 27 ++++- .../bundle/verify-consumer-evidence.py | 12 +- 16 files changed, 273 insertions(+), 67 deletions(-) create mode 100644 server/security/ubuntu-backports/bundle/installer/brotli_test.go create mode 100644 server/security/ubuntu-backports/bundle/installer/testdata/consumer-brotli-probe.c diff --git a/.github/workflows/publish-ubuntu-security-backports.yml b/.github/workflows/publish-ubuntu-security-backports.yml index 51b1a2048c..cb39482565 100644 --- a/.github/workflows/publish-ubuntu-security-backports.yml +++ b/.github/workflows/publish-ubuntu-security-backports.yml @@ -11,7 +11,7 @@ permissions: contents: read concurrency: - group: ubuntu-security-backports-v0.1.0-${{ github.event_name }} + group: ubuntu-security-backports-v0.1.1-${{ github.event_name }} cancel-in-progress: false jobs: @@ -81,7 +81,7 @@ jobs: artifact-metadata: write env: IMAGE: ghcr.io/pasturestack/ubuntu-security-backports - TAG: v0.1.0 + TAG: v0.1.1 steps: - name: Check out the exact protected publisher source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -270,7 +270,7 @@ jobs: if: always() && steps.inputs.outputs.work != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ubuntu-security-backports-v0.1.0-${{ github.sha }}-${{ github.run_id }} + name: ubuntu-security-backports-v0.1.1-${{ github.sha }}-${{ github.run_id }} path: | ${{ steps.inputs.outputs.work }}/context/out/ ${{ steps.inputs.outputs.work }}/publisher/ diff --git a/server/security/ubuntu-backports/README.md b/server/security/ubuntu-backports/README.md index a18ae9503c..4a1e929b95 100644 --- a/server/security/ubuntu-backports/README.md +++ b/server/security/ubuntu-backports/README.md @@ -14,7 +14,7 @@ Do not rerun an unchanged module that already has valid native PASS evidence. Its module source, lock, compiler/dependency inputs and expected artifact identity must still match. Changes to shared behavior require revalidating its consumers. -Select `glibc`, `rust-coreutils`, `jq`, `p11-kit` or `libxrender` in the +Select `glibc`, `rust-coreutils`, `jq`, `p11-kit` `libxrender` or `brotli` in the workflow. Source helpers run as a non-root user; Rust credential regression subprocesses alone receive privileges inside that temporary container. A failed helper retains logs and a failing result; exported files alone are not a PASS. @@ -51,3 +51,10 @@ providers, in addition to the installer unit tests. A bundle PASS is still not a product PASS: every consumer must retain its normal source/release gates, exact final-rootfs SBOM, raw scan, scoped VEX and affected runtime smoke. See `bundle/installer/README.md` for the fixed mount and command contract. + +The Brotli module retains Ubuntu 1.2.0-3build1 packaging and applies only the +accepted Google decoder fix for CVE-2026-111497. It exports libbrotli1 only, +requires ordinary upstream/API/streaming tests and all three original library +ABIs, and never executes a crafted CVE stream or installs packages on the host. +Each new bundle requires native and consumer qualifications followed by +separate immutable publication before a product may consume it. diff --git a/server/security/ubuntu-backports/bundle/Dockerfile.bundle b/server/security/ubuntu-backports/bundle/Dockerfile.bundle index 0d449a809c..b9656ed132 100644 --- a/server/security/ubuntu-backports/bundle/Dockerfile.bundle +++ b/server/security/ubuntu-backports/bundle/Dockerfile.bundle @@ -17,7 +17,7 @@ COPY --from=installer_build /out/install-existing-backports /install-existing-ba FROM scratch AS bundle ARG SOURCE_SHA LABEL org.opencontainers.image.title="Ubuntu security backport package inputs" \ - org.opencontainers.image.version="0.1.0" \ + org.opencontainers.image.version="0.1.1" \ org.opencontainers.image.source="https://github.com/PastureStack/server" \ org.opencontainers.image.revision="${SOURCE_SHA}" \ org.opencontainers.image.description="First-party qualified amd64 Ubuntu 26.04 deb inputs; consumer rootfs validation is separate." diff --git a/server/security/ubuntu-backports/bundle/installer/README.md b/server/security/ubuntu-backports/bundle/installer/README.md index 8c8706537b..ebc5f3c9dd 100644 --- a/server/security/ubuntu-backports/bundle/installer/README.md +++ b/server/security/ubuntu-backports/bundle/installer/README.md @@ -56,11 +56,11 @@ install a package, change the host, run apt, or invoke libc-maintainer scripts. the pinned Ubuntu 26.04 image and signed 2026-10-23 archive snapshot. Run it in the CI runner twice with `PROVIDER=gnu` and `PROVIDER=uutils`, context this folder, additional build context `qualified=`, target -`evidence`, and local output per provider. The GNU fixture has eleven selected -packages and no Rust; the uutils fixture has all twelve. Both consume the same +`evidence`, and local output per provider. The GNU fixture has twelve selected +packages and no Rust; the uutils fixture has all thirteen. Both consume the same 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. +the installed glibc loader/cache, jq, id, p11-kit, PCRE grep, NSS getent and Brotli valid-input API/streaming linkage. A separate test-only C probe uses the official jq release headers and `dlopen(..., RTLD_NOW)` without linking libjq or libm into the executable. Four threads each execute 128 runtime sorts using independent jq states, starting at @@ -71,3 +71,8 @@ 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. + +Bundle 0.1.1 adds only the Brotli family (`libbrotli1`) to the previous six +qualified families. An image without libbrotli1 receives no Brotli package. +Version 0.1.0 remains immutable and uses its original frozen verifier and +producer checksums; the 0.1.1 verifier accepts only the new exact catalog. diff --git a/server/security/ubuntu-backports/bundle/installer/brotli_test.go b/server/security/ubuntu-backports/bundle/installer/brotli_test.go new file mode 100644 index 0000000000..350c94f8ae --- /dev/null +++ b/server/security/ubuntu-backports/bundle/installer/brotli_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "reflect" + "strings" + "testing" +) + +func TestBrotliExistingOnlyAndCompanion(t *testing.T) { + for _, present := range []bool{false, true} { + names := []string{"libc6", "coreutils-from-gnu", "gnu-coreutils"} + if present { + names = append(names, "libbrotli1") + } + i, f := fixture(t, names...) + r, err := i.Execute(true) + must(t, err) + expected := []string{"libc6"} + if present { + expected = []string{"libbrotli1", "libc6"} + } + if !reflect.DeepEqual(r.Packages, expected) || r.BundleVersion != "0.1.1" || !r.ProviderPreserved { + t.Fatalf("unexpected existing-only result: %#v", r) + } + if _, exists := f.installed["libbrotli1"]; exists != present { + t.Fatal("Brotli package presence changed") + } + } + i, f := fixture(t, "libbrotli1", "libbrotli-dev") + p := f.installed["libbrotli-dev"] + p.Depends = "libbrotli1 (= 1.0)" + f.installed[p.Name] = p + _, err := i.Execute(true) + if err == nil || !strings.Contains(err.Error(), "matching companion archive is absent") || f.installedBatch != nil { + t.Fatalf("missing exact-version development companion was accepted: %v", err) + } +} diff --git a/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile b/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile index 3e46135b58..346ca88460 100644 --- a/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile +++ b/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile @@ -5,7 +5,9 @@ FROM golang:1.27.3-bookworm@sha256:972b4110ba0f47539a975c31e52c64765e1ff19f2d744 # qualified package. The probe dynamically loads libjq in the Ubuntu consumer; # neither libjq nor libm is linked into the executable at build time. ADD --checksum=sha256:71b8d6e8f5fe81f6c6d0d110e3892251f6ce76ed095abd315e26e6e1193af3af https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-1.8.2.tar.gz /tmp/jq.tar.gz +ADD --checksum=sha256:eb5f7dadf215d0670665fd81566e1fe2dfdc154d983f09142de7299df4c182e6 https://archive.ubuntu.com/ubuntu/pool/main/b/brotli/brotli_1.2.0.orig.tar.gz /tmp/brotli.tar.gz COPY testdata/consumer-libjq-probe.c /src/consumer-libjq-probe.c +COPY testdata/consumer-brotli-probe.c /src/consumer-brotli-probe.c RUN set -eux; \ mkdir /tmp/headers; \ tar --extract --gzip --file /tmp/jq.tar.gz --directory /tmp/headers --strip-components=2 jq-1.8.2/src/jq.h jq-1.8.2/src/jv.h; \ @@ -13,6 +15,13 @@ RUN set -eux; \ /src/consumer-libjq-probe.c -ldl -o /consumer-libjq-probe; \ readelf --dynamic /consumer-libjq-probe >/tmp/probe-dynamic.txt; \ if grep -E 'NEEDED.*\[(libjq|libm)\.' /tmp/probe-dynamic.txt; then exit 1; fi +RUN set -eux; \ + mkdir /tmp/brotli-headers; \ + tar --extract --gzip --file /tmp/brotli.tar.gz --directory /tmp/brotli-headers --strip-components=3 google-brotli-028fb5a/c/include; \ + cc -std=gnu11 -Wall -Wextra -Werror -O2 -isystem /tmp/brotli-headers \ + /src/consumer-brotli-probe.c -ldl -o /consumer-brotli-probe; \ + readelf --dynamic /consumer-brotli-probe >/tmp/brotli-probe-dynamic.txt; \ + if grep -E 'NEEDED.*\[libbrotli' /tmp/brotli-probe-dynamic.txt; then exit 1; fi FROM ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b AS consumer ARG PROVIDER=gnu @@ -61,7 +70,7 @@ set -- \ 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.1 ca-certificates grep + 'libxrender1=1:0.9.12-1build1' libbrotli1=1.2.0-3build1 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' ;; @@ -72,6 +81,7 @@ rm -rf /var/lib/apt/lists/* /tmp/ca-bootstrap /tmp/ca-certificates.deb SH COPY consumer-smoke.sh /consumer-smoke.sh COPY --from=embedding_probe /consumer-libjq-probe /consumer-libjq-probe +COPY --from=embedding_probe /consumer-brotli-probe /consumer-brotli-probe RUN --mount=from=qualified,source=/out,target=/opt/pasturestack/ubuntu-security-backports,ro \ /bin/bash /consumer-smoke.sh "$PROVIDER" diff --git a/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh b/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh index 8d927d17cc..cbcbcbf64e 100644 --- a/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh +++ b/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh @@ -36,13 +36,13 @@ installed gnu-coreutils if [[ "$provider" == gnu ]]; then absent rust-coreutils absent coreutils-from-uutils - expected=11 + expected=12 else installed rust-coreutils absent coreutils-from-gnu - expected=12 + expected=13 fi -for package in libc6-dev libc-dev-bin libpcre2-dev libjq-dev libp11-kit-dev libxrender-dev; do +for package in libc6-dev libc-dev-bin libpcre2-dev libjq-dev libp11-kit-dev libxrender-dev libbrotli-dev; do absent "$package" done input_manifest_sha="$(sha256sum "$data/manifest.json" | awk '{print $1}')" @@ -81,7 +81,7 @@ test -z "$(dpkg --audit)" test -s /etc/ld.so.cache sha256sum /etc/ld.so.cache >"$evidence/ld-so-cache-after.sha256" ldconfig -p >"$evidence/ldconfig.txt" -for soname in libc.so.6 libpcre2-8.so.0 libpcre2-posix.so.3 libjq.so.1 libp11-kit.so.0 libXrender.so.1; do +for soname in libc.so.6 libpcre2-8.so.0 libpcre2-posix.so.3 libjq.so.1 libp11-kit.so.0 libXrender.so.1 libbrotlicommon.so.1 libbrotlidec.so.1 libbrotlienc.so.1; do grep -F "$soname " "$evidence/ldconfig.txt" done getconf GNU_LIBC_VERSION | tee "$evidence/glibc-version.txt" | grep -Fx 'glibc 2.43' @@ -90,6 +90,7 @@ ldd /usr/lib/x86_64-linux-gnu/libXrender.so.1 >"$evidence/libxrender-loader.txt" if grep -F 'not found' "$evidence/libxrender-loader.txt"; then exit 1; fi printf '%s\n' '{"numbers":[1,2,3]}' | jq -e '.numbers | add == 6' timeout 30 /consumer-libjq-probe | tee "$evidence/libjq-embedding.txt" +timeout 30 /consumer-brotli-probe | tee "$evidence/brotli-linkage.txt" test "$(id -u)" = 0 test "$(id -g)" = 0 id -G >"$evidence/id-groups.txt" @@ -99,5 +100,5 @@ getent passwd root | tee "$evidence/getent-root.txt" | grep -E '^root:[^:]*:0:0: test "$input_manifest_sha" = "$(sha256sum "$data/manifest.json" | awk '{print $1}')" jq -n --arg provider "$provider" --arg manifest "$input_manifest_sha" \ --slurpfile receipt "$evidence/install.json" \ - '{schemaVersion:1,provider:$provider,status:"PASS",inputManifestSha256:$manifest,selectedPackages:$receipt[0].packages,checks:["batch-install","verify-installed","package-set","provider","docs-restored","glibc-loader","jq","libjq-RTLD_NOW-TLS-threads","id","p11-kit","grep","getent"]}' >"$evidence/result.json" + '{schemaVersion:1,provider:$provider,status:"PASS",inputManifestSha256:$manifest,selectedPackages:$receipt[0].packages,checks:["batch-install","verify-installed","package-set","provider","docs-restored","glibc-loader","jq","libjq-RTLD_NOW-TLS-threads","id","p11-kit","grep","getent","brotli-RTLD_NOW-valid-roundtrip"]}' >"$evidence/result.json" cat "$evidence/result.json" diff --git a/server/security/ubuntu-backports/bundle/installer/installer.go b/server/security/ubuntu-backports/bundle/installer/installer.go index 5c48e9bef0..a3c8ab079f 100644 --- a/server/security/ubuntu-backports/bundle/installer/installer.go +++ b/server/security/ubuntu-backports/bundle/installer/installer.go @@ -391,7 +391,7 @@ func unchangedInventory(before, after map[string]Installed, selected []Package) return nil } func (i Installer) Execute(install bool) (Receipt, error) { - r := Receipt{SchemaVersion: 1, BundleVersion: "0.1.0", Packages: []string{}, BeforeMissingDocumentation: []string{}, BeforeMissingTranslations: []string{}, ExactPathIncludes: []string{}} + r := Receipt{SchemaVersion: 1, BundleVersion: "0.1.1", Packages: []string{}, BeforeMissingDocumentation: []string{}, BeforeMissingTranslations: []string{}, ExactPathIncludes: []string{}} if err := i.platform(); err != nil { return r, err } diff --git a/server/security/ubuntu-backports/bundle/installer/installer_test.go b/server/security/ubuntu-backports/bundle/installer/installer_test.go index 56ce3b1f5a..148057bd33 100644 --- a/server/security/ubuntu-backports/bundle/installer/installer_test.go +++ b/server/security/ubuntu-backports/bundle/installer/installer_test.go @@ -159,7 +159,7 @@ func fixture(t *testing.T, names ...string) (Installer, *fakeRunner) { must(t, os.MkdirAll(filepath.Join(root, "var/lib/dpkg/alternatives"), 0755)) write(t, root, "usr/bin/ls", []byte("provider command")) f := &fakeRunner{t: t, root: root, bundle: bundle, installed: map[string]Installed{}, packages: map[string]Package{}, payloads: map[string]Payload{}, tars: map[string][]byte{}, contents: map[string][]byte{}, beforeDiff: map[string]string{}, afterDiff: map[string]string{}} - m := Manifest{SchemaVersion: 1, BundleVersion: "0.1.0", Platform: Platform{"ubuntu", "26.04", "amd64"}, Families: []Family{}, Packages: []Package{}} + m := Manifest{SchemaVersion: 1, BundleVersion: "0.1.1", Platform: Platform{"ubuntu", "26.04", "amd64"}, Families: []Family{}, Packages: []Package{}} q := Qualification{SchemaVersion: 1, BundleVersion: m.BundleVersion, Platform: m.Platform, Status: "qualified", Families: []QualifiedFamily{}} lock := jsonFile(t, bundle, "source-inputs.lock.json", map[string]string{"identity": "source lock"}) q.SourceLockSHA256 = lock.SHA256 diff --git a/server/security/ubuntu-backports/bundle/installer/schema.go b/server/security/ubuntu-backports/bundle/installer/schema.go index 748c608bc8..ebad45b03b 100644 --- a/server/security/ubuntu-backports/bundle/installer/schema.go +++ b/server/security/ubuntu-backports/bundle/installer/schema.go @@ -22,6 +22,7 @@ var families = map[string][]string{ "rust-coreutils": {"rust-coreutils"}, "jq": {"jq", "libjq1"}, "p11-kit": {"libp11-kit0", "p11-kit", "p11-kit-modules"}, "libxrender": {"libxrender1"}, + "brotli": {"libbrotli1"}, } type Platform struct{ Distribution, Release, Architecture string } @@ -201,7 +202,7 @@ func loadManifest(root string) (Manifest, map[string]Payload, error) { if err = decode(b, &m, true); err != nil { return m, nil, err } - if m.SchemaVersion != 1 || m.BundleVersion != "0.1.0" || !validatePlatform(m.Platform) || len(m.Families) != 6 || len(m.Packages) != 12 || m.Qualification.Path != "qualification.json" { + if m.SchemaVersion != 1 || m.BundleVersion != "0.1.1" || !validatePlatform(m.Platform) || len(m.Families) != 7 || len(m.Packages) != 13 || m.Qualification.Path != "qualification.json" { return m, nil, fmt.Errorf("unsupported bundle identity/catalog") } qb, err := boundRead(root, m.Qualification, 16<<20) @@ -212,7 +213,7 @@ func loadManifest(root string) (Manifest, map[string]Payload, error) { if err = decode(qb, &q, false); err != nil { return m, nil, err } - if q.SchemaVersion != 1 || q.BundleVersion != m.BundleVersion || !validatePlatform(q.Platform) || q.Status != "qualified" || len(q.Families) != 6 { + if q.SchemaVersion != 1 || q.BundleVersion != m.BundleVersion || !validatePlatform(q.Platform) || q.Status != "qualified" || len(q.Families) != 7 { return m, nil, fmt.Errorf("bundle is not qualified") } if _, err = boundRead(root, Ref{"source-inputs.lock.json", q.SourceLockSHA256}, 8<<20); err != nil { diff --git a/server/security/ubuntu-backports/bundle/installer/testdata/consumer-brotli-probe.c b/server/security/ubuntu-backports/bundle/installer/testdata/consumer-brotli-probe.c new file mode 100644 index 0000000000..cd4a0f554f --- /dev/null +++ b/server/security/ubuntu-backports/bundle/installer/testdata/consumer-brotli-probe.c @@ -0,0 +1,31 @@ +/* Disposable consumer linkage of the actual installed libraries; valid input only. */ +#include +#include +#include +#include +#include + +int main(void) { + const char *names[] = {"libbrotlicommon.so.1", "libbrotlidec.so.1", "libbrotlienc.so.1"}; + void *handles[3]; + for (size_t n = 0; n < 3; ++n) { + handles[n] = dlopen(names[n], RTLD_NOW | RTLD_LOCAL); + if (!handles[n]) { fprintf(stderr, "%s\n", dlerror()); return 1; } + } + BROTLI_BOOL (*compress)(int, int, BrotliEncoderMode, size_t, const uint8_t *, size_t *, uint8_t *) = + (BROTLI_BOOL (*)(int, int, BrotliEncoderMode, size_t, const uint8_t *, size_t *, uint8_t *)) + dlsym(handles[2], "BrotliEncoderCompress"); + BrotliDecoderResult (*decompress)(size_t, const uint8_t *, size_t *, uint8_t *) = + (BrotliDecoderResult (*)(size_t, const uint8_t *, size_t *, uint8_t *)) + dlsym(handles[1], "BrotliDecoderDecompress"); + if (!compress || !decompress) return 2; + const uint8_t input[] = "PastureStack ordinary installed Brotli linkage control."; + uint8_t compressed[1024], output[1024]; + size_t encoded = sizeof(compressed), decoded = sizeof(output); + if (!compress(6, BROTLI_DEFAULT_WINDOW, BROTLI_MODE_GENERIC, sizeof(input), input, &encoded, compressed) || + decompress(encoded, compressed, &decoded, output) != BROTLI_DECODER_RESULT_SUCCESS || + decoded != sizeof(input) || memcmp(input, output, decoded)) return 3; + for (size_t n = 3; n > 0; --n) if (dlclose(handles[n - 1])) return 4; + puts("PASS: three installed Brotli SONAMEs RTLD_NOW, ordinary compression/decompression"); + return 0; +} diff --git a/server/security/ubuntu-backports/bundle/qualify-native-packages.py b/server/security/ubuntu-backports/bundle/qualify-native-packages.py index 4b722d85a3..ed189584a6 100644 --- a/server/security/ubuntu-backports/bundle/qualify-native-packages.py +++ b/server/security/ubuntu-backports/bundle/qualify-native-packages.py @@ -35,14 +35,14 @@ GUARD_SPEC = importlib.util.spec_from_file_location('backport_metadata_guard', HERE / 'verify-consumer-evidence.py') GUARD = importlib.util.module_from_spec(GUARD_SPEC) GUARD_SPEC.loader.exec_module(GUARD) -WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.0' +WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.1' REPO = 'PastureStack/server' PLATFORM = {'distribution': 'ubuntu', 'release': '26.04', 'architecture': 'amd64'} FAMILIES = { 'pcre2': ['libpcre2-8-0', 'libpcre2-posix3'], 'glibc': ['libc6', 'libc-bin', 'libc-gconv-modules-extra'], 'rust-coreutils': ['rust-coreutils'], 'jq': ['jq', 'libjq1'], - 'p11-kit': ['libp11-kit0', 'p11-kit', 'p11-kit-modules'], 'libxrender': ['libxrender1'], + 'p11-kit': ['libp11-kit0', 'p11-kit', 'p11-kit-modules'], 'libxrender': ['libxrender1'], 'brotli': ['libbrotli1'], } BASE = 'server/security/ubuntu-backports/' PCRE2_STAGE_STEPS = [ @@ -109,7 +109,7 @@ def read_zip(path, digest): def load_lock(ready=False): lock = json.loads(LOCK_FILE.read_text(encoding='utf-8')) require(lock['schemaVersion'] == 1 and lock['repository'] == REPO - and lock['bundleVersion'] == '0.1.0' and lock['platform'] == PLATFORM, + and lock['bundleVersion'] == '0.1.1' and lock['platform'] == PLATFORM, 'Unexpected bundle identity') require(set(lock['nativeInputs']) == set(FAMILIES) - {'pcre2'}, 'Wrong native family set') for family, item in lock['nativeInputs'].items(): @@ -117,9 +117,9 @@ def load_lock(ready=False): require(re.fullmatch('[0-9a-f]{40}', item['sourceCommit']) and type(item['runId']) is int, 'Native run/source must be immutable') if item['status'] == 'pending': - require(family == 'glibc' and item['artifactId'] is None and item['zipSha256'] is None, + require(family in ('glibc', 'brotli') and item['artifactId'] is None and item['zipSha256'] is None, 'Pending input must not invent an artifact') - require(not ready, 'glibc native artifact is pending; publication is blocked') + require(not ready, family + ' native artifact is pending; publication is blocked') continue require(item['status'] == 'qualified' and type(item['artifactId']) is int and type(item['jobId']) is int and re.fullmatch('[0-9a-f]{64}', item['zipSha256']), @@ -395,7 +395,7 @@ def registry_manifest(mode): token = json.load(response)['token'] headers = {'Authorization': 'Bearer ' + token, 'Accept': 'application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json'} - request = urllib.request.Request('https://ghcr.io/v2/pasturestack/ubuntu-security-backports/manifests/v0.1.0', headers=headers) + request = urllib.request.Request('https://ghcr.io/v2/pasturestack/ubuntu-security-backports/manifests/v0.1.1', headers=headers) try: with urllib.request.urlopen(request, timeout=30) as response: data = response.read(4 * 1024 * 1024 + 1) @@ -405,7 +405,7 @@ def registry_manifest(mode): require(mode == 'absent' and error.code == 404 and codes and codes <= {'MANIFEST_UNKNOWN', 'NAME_UNKNOWN'}, 'Registry response does not prove tag absence/public visibility') print('NEW_NUMERIC_TAG_CONFIRMED'); return - require(mode != 'absent', 'Refusing to overwrite existing v0.1.0') + require(mode != 'absent', 'Refusing to overwrite existing v0.1.1') digest = json.loads((WORK / 'publisher' / 'image-metadata.json').read_text())['containerimage.digest'] require(re.fullmatch('sha256:[0-9a-f]{64}', digest) and 'sha256:' + sha(data) == digest, 'Registry manifest differs from the exact pushed digest') @@ -442,6 +442,18 @@ def check_owning_tests(family, files, provenance, directory): and smoke['publicAbiSymbols'] > 0, 'jq original ABI/system oniguruma smoke missing') return {'status': 'qualified', 'owningTests': expected} helper = module_helper(family) + if family == 'brotli': + report = json.loads(files['brotli-tests.json']) + suites = helper.owning_results(files['cmake-tests.xml'].decode(), files['python-tests.log'].decode()) + require(report == provenance['tests'] and report['status'] == 'PASS' + and report['owningSuites'] == suites and report['runtimeInstalled'] is False + and report['exploitExecuted'] is False + and len(report['abi']) == 3 + and [a['soname'] for a in report['abi']] == helper.SONAMES + and all(a['status'] == 'PASS' and a['exports'] > 0 for a in report['abi']) + and report['packageApi'] == 'PASS: 15 valid roundtrips, 45 streaming roundtrips, three linked SONAMEs', + 'Brotli original owning suite/three-library ABI/valid-input API evidence missing') + return report if family == 'p11-kit': suite = provenance['tests']['fullOwningSuite'] (directory / 'test-suite.log').write_bytes(files['test-suite.log']) @@ -763,24 +775,24 @@ def qualify(lock): glibc['codeAbsence'] = glibc_nscd_qualification(lock['nativeInputs']['glibc'], packages, glibc_payloads) absence_purls = GUARD.validate_glibc_nscd_qualification({q['name']: q for q in qualifications}, {p['name']: p for p in packages}, lock) - write_json(output / 'qualification.json', {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'platform': PLATFORM, + write_json(output / 'qualification.json', {'schemaVersion': 1, 'bundleVersion': '0.1.1', 'platform': PLATFORM, 'status': 'qualified', 'families': qualifications, 'sourceLockSha256': file_sha(LOCK_FILE), 'installer': installer_proof, 'consumerEvidenceVerifier': guard_proof, 'consumerStatus': 'Package inputs only; consumer installation, rootfs scan and product acceptance remain required.'}) shutil.copyfile(LOCK_FILE, output / 'source-inputs.lock.json') - write_json(output / 'manifest.json', {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'platform': PLATFORM, + write_json(output / 'manifest.json', {'schemaVersion': 1, 'bundleVersion': '0.1.1', 'platform': PLATFORM, 'qualification': {'path': 'qualification.json', 'sha256': file_sha(output / 'qualification.json')}, 'families': families, 'packages': packages}) components = [{'type': 'library', 'name': p['name'], 'version': p['version'], 'purl': purl(p), 'bom-ref': purl(p), 'hashes': [{'alg': 'SHA-256', 'content': p['archive']['sha256']}]} for p in packages] - components.append({'type': 'application', 'name': 'install-existing-backports', 'version': '0.1.0', - 'bom-ref': 'pkg:generic/pasturestack/backport-installer@0.1.0', - 'purl': 'pkg:generic/pasturestack/backport-installer@0.1.0', + components.append({'type': 'application', 'name': 'install-existing-backports', 'version': '0.1.1', + 'bom-ref': 'pkg:generic/pasturestack/backport-installer@0.1.1', + 'purl': 'pkg:generic/pasturestack/backport-installer@0.1.1', 'hashes': [{'alg': 'SHA-256', 'content': sha(binary)}], 'properties': [{'name': 'pasturestack:go-toolchain', 'value': 'go1.27.3'}]}) write_json(output / 'sbom.cdx.json', {'bomFormat': 'CycloneDX', 'specVersion': '1.6', 'version': 1, - 'metadata': {'component': {'type': 'data', 'name': 'ubuntu-security-backports', 'version': '0.1.0'}}, + 'metadata': {'component': {'type': 'data', 'name': 'ubuntu-security-backports', 'version': '0.1.1'}}, 'components': components}) statements = [] for family in FAMILIES: @@ -792,7 +804,7 @@ def qualify(lock): 'status': 'fixed', 'action_statement': item['vexScope']}) statements.append(GUARD.glibc_nscd_statement(absence_purls)) write_json(output / 'openvex.json', {'@context': 'https://openvex.dev/ns/v0.2.0', - '@id': 'https://github.com/PastureStack/server/ubuntu-security-backports/0.1.0', + '@id': 'https://github.com/PastureStack/server/ubuntu-security-backports/0.1.1', 'author': 'PastureStack', 'timestamp': lock['vexTimestamp'], 'version': 1, 'statements': statements}) write_json(output / 'tracked-scopes.json', lock['trackedScopes']) write_checksums(output) @@ -807,7 +819,7 @@ def record_consumer_tests(): manifest_sha = file_sha(output / 'manifest.json') records = [] checks = ['batch-install', 'verify-installed', 'package-set', 'provider', 'docs-restored', - 'glibc-loader', 'jq', 'libjq-RTLD_NOW-TLS-threads', 'id', 'p11-kit', 'grep', 'getent'] + 'glibc-loader', 'jq', 'libjq-RTLD_NOW-TLS-threads', 'id', 'p11-kit', 'grep', 'getent', 'brotli-RTLD_NOW-valid-roundtrip'] for provider in ['gnu', 'uutils']: source = WORK / ('consumer-' + provider) result = json.loads((source / 'result.json').read_text()) diff --git a/server/security/ubuntu-backports/bundle/source-inputs.lock.json b/server/security/ubuntu-backports/bundle/source-inputs.lock.json index afd970aab0..79f5f7c70a 100644 --- a/server/security/ubuntu-backports/bundle/source-inputs.lock.json +++ b/server/security/ubuntu-backports/bundle/source-inputs.lock.json @@ -1,5 +1,5 @@ { - "bundleVersion": "0.1.0", + "bundleVersion": "0.1.1", "consumerFixture": { "gnuBaselineProviderSetup": { "scope": "Disposable GNU fixture construction only; the backport installer must preserve the selected provider.", @@ -45,21 +45,21 @@ "snapshotURL": "https://snapshot.ubuntu.com/ubuntu/20261023T000000Z", "ubuntuImage": "ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b" }, - "image": "ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0", + "image": "ghcr.io/pasturestack/ubuntu-security-backports:v0.1.1", "nativeInputs": { "glibc": { "artifactId": 12573643405, "artifactName": "ubuntu-native-glibc-507cadcb3eaafcd6a85c453f2f180a8aef415d88-40122645083", "ciClosure": { ".github/workflows/ubuntu-backport-native.yml": { - "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79", + "currentSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", "nativeSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79", - "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files." + "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." }, "server/security/ubuntu-backports/Dockerfile.native-ci": { - "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b", + "currentSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", "nativeSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b", - "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files." + "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." } }, "codeAbsenceProof": { @@ -150,14 +150,14 @@ "artifactName": "ubuntu-native-jq-bc129a88a5ec8c4e64b3ed89bfd97c7e82362d7c-40116776455", "ciClosure": { ".github/workflows/ubuntu-backport-native.yml": { - "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79", + "currentSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", "nativeSha256": "e2b32c1f0ccafc21503a4cd4b3eb5fb10a82aec82b7eb81c74dc634e7afa4c05", - "reviewedDifference": "Reviewed exact diff: workflow adds other module selection/initial-PR scheduling; recipe adds p11-kit/libxrender-only tools, Rust-only CI marker, builder inventory and failed-glibc diagnostics. jq effective source/build dependencies and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: workflow adds other module selection/initial-PR scheduling; recipe adds p11-kit/libxrender-only tools, Rust-only CI marker, builder inventory and failed-glibc diagnostics. jq effective source/build dependencies and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." }, "server/security/ubuntu-backports/Dockerfile.native-ci": { - "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b", + "currentSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", "nativeSha256": "b3dd3e1f7bbad9ace7c35e32f061d307862cad31b60c4818a0a666ed4b25e0fb", - "reviewedDifference": "Reviewed exact diff: workflow adds other module selection/initial-PR scheduling; recipe adds p11-kit/libxrender-only tools, Rust-only CI marker, builder inventory and failed-glibc diagnostics. jq effective source/build dependencies and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: workflow adds other module selection/initial-PR scheduling; recipe adds p11-kit/libxrender-only tools, Rust-only CI marker, builder inventory and failed-glibc diagnostics. jq effective source/build dependencies and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." } }, "copyright": { @@ -251,14 +251,14 @@ "artifactName": "ubuntu-native-libxrender-07e4a7afa827cb45514a72aba12f03b1d30d121d-40120642373", "ciClosure": { ".github/workflows/ubuntu-backport-native.yml": { - "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79", + "currentSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", "nativeSha256": "f06a225d71f28b31f1592025ed272055aa54266c1c112bc08c70e55529d885f4", - "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." }, "server/security/ubuntu-backports/Dockerfile.native-ci": { - "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b", + "currentSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", "nativeSha256": "bca055ed1bd8fd213a906eceb23c5ff3f9449d06acb51e249c976c3174d406bb", - "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." } }, "copyright": { @@ -313,14 +313,14 @@ "artifactName": "ubuntu-native-p11-kit-2a47f3cd99b6afc47b54ee6d8837b834e95c50f2-40121338859", "ciClosure": { ".github/workflows/ubuntu-backport-native.yml": { - "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79", + "currentSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", "nativeSha256": "f06a225d71f28b31f1592025ed272055aa54266c1c112bc08c70e55529d885f4", - "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." }, "server/security/ubuntu-backports/Dockerfile.native-ci": { - "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b", + "currentSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", "nativeSha256": "bca055ed1bd8fd213a906eceb23c5ff3f9449d06acb51e249c976c3174d406bb", - "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." } }, "copyright": { @@ -524,14 +524,14 @@ "artifactName": "ubuntu-native-rust-coreutils-07e4a7afa827cb45514a72aba12f03b1d30d121d-40120637646", "ciClosure": { ".github/workflows/ubuntu-backport-native.yml": { - "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79", + "currentSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", "nativeSha256": "f06a225d71f28b31f1592025ed272055aa54266c1c112bc08c70e55529d885f4", - "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." }, "server/security/ubuntu-backports/Dockerfile.native-ci": { - "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b", + "currentSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", "nativeSha256": "bca055ed1bd8fd213a906eceb23c5ff3f9449d06acb51e249c976c3174d406bb", - "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged." + "reviewedDifference": "Reviewed exact diff: removes tar selection, adds builder inventory and failed-glibc diagnostics only. This family effective build dependency setup, source inputs and owning commands are unchanged. Additive Brotli-only selection and native MODULE allowlist; this original family source/build/test behavior is unchanged." } }, "copyright": { @@ -600,6 +600,72 @@ "status": "qualified", "vexScope": "Exact qualified rust-coreutils package bytes: the new id CVEs use the official ff382 backport and packaged GID checks; the other 18 have an independently reviewed exact Ubuntu orig + enabled quilt source/control audit, bound to this native lock, provenance and package SHA. Source closure does not claim new CVE-specific dynamic execution: retain 84 upstream ignored, 17 original filtered tests, missing dd/split unit execution and the logname real-login limitation. Consumer images still require native Trivy --vex, installation and product acceptance.", "zipSha256": "7f4b4780a492425bb9de239d67c6d594ccd3e8c0c85ac03eaa85035248ac5e2b" + }, + "brotli": { + "status": "qualified", + "sourceCommit": "7f90a147c69ff1fdbfbf8a7e48b0bf12a95fbc56", + "runId": 40174455200, + "jobId": 121112022951, + "artifactId": 12592011612, + "zipSha256": "03ad6c149f55f3f05483cd1aa39d300c978f953b60c88c0acdc4edbfe2d569cb", + "runtimePackages": [ + "libbrotli1" + ], + "sourceFiles": { + "server/security/ubuntu-backports/brotli/brotli.lock.json": "0b709047ae7f0e6f068dcfd8b06379007947256feb3920667a48381ed60dc6d5", + "server/security/ubuntu-backports/brotli/build-brotli-backport.py": "0db2bc183e1e6ff4e7cf2c100107e2473bd7325f459c6d9e6f14a7e63cabd223", + "server/security/ubuntu-backports/brotli/roundtrip.c": "a2891bf18f18b7ecd3c50871cc00c64d922dba0b4b2d899cb6a57eaec6da0a31", + "server/security/ubuntu-backports/brotli/test_brotli_backport.py": "a43213fa3da19ff139782e0a0a022165d5c954f484fb9cdf3bff64ff5efe62b9", + "server/security/ubuntu-backports/source_helpers.py": "d2b3f310f8ed99a50a696fe14094b7117ec5d7ed576e6080d5540c0e82891486", + "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2" + }, + "lockSha256": "0b709047ae7f0e6f068dcfd8b06379007947256feb3920667a48381ed60dc6d5", + "fixedVulnerabilities": [ + "CVE-2026-111497" + ], + "packages": [ + { + "file": "libbrotli1_1.2.0-3build1+pasturestack.1_amd64.deb", + "name": "libbrotli1", + "source": "brotli", + "version": "1.2.0-3build1+pasturestack.1", + "sha256": "a2dca2c08cbc5ffe9a57432e80b989c458f7ed38c39bad33f3a3fa006fea507b" + } + ], + "vexScope": "Exact accepted Google decoder security commit, preserved Ubuntu packaging and qualified native ordinary valid-input/API/streaming/three-library ABI controls. Only this first-party libbrotli1 package; no CVE trigger was executed.", + "ciClosure": { + ".github/workflows/ubuntu-backport-native.yml": { + "nativeSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", + "currentSha256": "d13cdeced4645989feff5846ad7452bc014ae2095e3f13ade370cc864cc191b2", + "reviewedDifference": "Identical native CI recipe." + }, + "server/security/ubuntu-backports/Dockerfile.native-ci": { + "nativeSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", + "currentSha256": "365dd371e17a58c7f2f7523c52a3511b513b16bef765e904dcfea23feb11cc5d", + "reviewedDifference": "Identical native CI recipe." + } + }, + "artifactName": "ubuntu-native-brotli-7f90a147c69ff1fdbfbf8a7e48b0bf12a95fbc56-40174455200", + "runConclusion": "success", + "copyright": { + "file": "COPYRIGHT", + "sha256": "24a64e5bb83d0960d1835696a1e23c0896ad6055b0ca47c66ab0eb9a766324b1" + }, + "evidence": { + "COPYRIGHT": "24a64e5bb83d0960d1835696a1e23c0896ad6055b0ca47c66ab0eb9a766324b1", + "SHA256SUMS": "db1cc580ab7800f057d8711d6644d8852d57e9c20fe138148091ef762d3e5825", + "brotli-tests.json": "eeb45967ee74e0a9740ae2b9a449d012e79c6f1cb460b8c91f29fe8a38c8e67c", + "brotli.lock.json": "0b709047ae7f0e6f068dcfd8b06379007947256feb3920667a48381ed60dc6d5", + "brotli_1.2.0-3build1+pasturestack.1_amd64.buildinfo": "93d9788ce3be8a5031bd32bfde494603dc022f3595c3d68fd317044bd163a5fa", + "brotli_1.2.0-3build1+pasturestack.1_amd64.changes": "9e58e936c4d0c2e0b78857dbf220be9b6765119b75837af513280d31d4718dd2", + "build.log": "116f045b8e49ed5337478a64969249919148fcbeaf90a5dedecd48c0cf680edc", + "builder-packages.tsv": "576f5f872ce35505aab6d442d2fd119cffdc6b2013f50c98fb00d1adb192f02f", + "cmake-tests.log": "ff22bf7b56a8129502f9814277293a0da75cf55a053f8a163fb65d024c6b90bf", + "cmake-tests.xml": "489484b4eb71a4c0e04dff39b70e955ed12c62e8d38e60783bae30fbe6c36f89", + "manifest.tsv": "dbf238ddb9bce5b15410fd137fa0ec0754dc2cf699a1dfcfef7f50f7b6d6b003", + "provenance.json": "bc440934207f0bd57074b148990c3c2eaae3b10f078de5de16d0ad2f829fbb14", + "python-tests.log": "edd3a7d019490012c9978bfa6e84db3c1dc4c4f219800eeb1e4a17d50c5a88d4" + } } }, "pcre2": { 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 7950142cc7..e1f37dc7d2 100644 --- a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py +++ b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py @@ -44,6 +44,25 @@ def pcre2_api(stage): class Boundaries(unittest.TestCase): + def test_brotli_qualification_replays_owning_receipts_and_three_library_contract(self): + xml = '' + '' * 12 + '' + python = 'Ran 135 tests in 0.1s\n\nOK\n' + report = {'status': 'PASS', 'owningSuites': {'status': 'PASS', 'cmakeCases': 12, + 'pythonCases': 135, 'addedSkips': 0}, 'runtimeInstalled': False, 'exploitExecuted': False, + 'abi': [{'soname': n, 'exports': 1, 'status': 'PASS'} for n in Q.module_helper('brotli').SONAMES], + 'packageApi': 'PASS: 15 valid roundtrips, 45 streaming roundtrips, three linked SONAMEs'} + files = {'brotli-tests.json': json.dumps(report).encode(), 'cmake-tests.xml': xml.encode(), + 'python-tests.log': python.encode()} + result = Q.check_owning_tests('brotli', files, {'tests': report}, self.root / 'positive') + self.assertEqual(result, report) + for index, bad in enumerate([dict(report, runtimeInstalled=True), dict(report, exploitExecuted=True), + dict(report, abi=report['abi'][:2]), dict(report, packageApi='PASS')]): + changed = dict(files, **{'brotli-tests.json': json.dumps(bad).encode()}) + with self.assertRaises(ValueError): + Q.check_owning_tests('brotli', changed, {'tests': bad}, self.root / ('negative-' + str(index))) + changed = dict(files, **{'cmake-tests.xml': xml.replace('/>', '>', 1).encode()}) + with self.assertRaises(ValueError): Q.check_owning_tests('brotli', changed, {'tests': report}, self.root / 'skip') + def setUp(self): self.temp = tempfile.TemporaryDirectory(prefix='bundle-unit-') self.addCleanup(self.temp.cleanup) @@ -411,7 +430,7 @@ def consumer_fixture(self, work, wrong=None): result = {'schemaVersion': 1, 'provider': provider, 'status': 'PASS', 'inputManifestSha256': digest, 'selectedPackages': selected, 'checks': ['batch-install', 'verify-installed', 'package-set', 'provider', 'docs-restored', 'glibc-loader', 'jq', 'libjq-RTLD_NOW-TLS-threads', - 'id', 'p11-kit', 'grep', 'getent']} + 'id', 'p11-kit', 'grep', 'getent', 'brotli-RTLD_NOW-valid-roundtrip']} if wrong and provider == 'gnu': wrong(result) source = work / ('consumer-' + provider); source.mkdir() Q.write_json(source / 'result.json', result) diff --git a/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py b/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py index 7b6052e551..01daa8c2a4 100644 --- a/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py +++ b/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py @@ -36,7 +36,7 @@ def semantic_receipt(vex_sha, source_lock_sha): def fixture(absence=False): - source = {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'platform': guard.PLATFORM} + source = {'schemaVersion': 1, 'bundleVersion': '0.1.1', 'platform': guard.PLATFORM} manifest = dict(source, families=[], packages=[]) qualification = dict(source, status='qualified', sourceLockSha256=guard.sha(encoded(source)), families=[]) for family, names in guard.FAMILIES.items(): @@ -51,9 +51,9 @@ def fixture(absence=False): 'archive': {'sha256': '3' * 64}, 'payload': {'sha256': '4' * 64}}) manifest['qualification'] = {'path': 'qualification.json', 'sha256': guard.sha(encoded(qualification))} selected = ['libc6', 'libc-bin', 'libc-gconv-modules-extra', 'libpcre2-8-0', 'rust-coreutils'] - install = {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'status': 'installed-and-verified', + install = {'schemaVersion': 1, 'bundleVersion': '0.1.1', 'status': 'installed-and-verified', 'providerPreserved': True, 'configurationPreserved': True, 'packages': selected} - verified = {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'status': 'verified', 'packages': selected} + verified = {'schemaVersion': 1, 'bundleVersion': '0.1.1', 'status': 'verified', 'packages': selected} vex = {'@context': 'https://openvex.dev/ns/v0.2.0', 'statements': [ {'vulnerability': {'name': 'CVE-2026-35370'}, 'status': 'fixed', 'action_statement': 'Exact official patch qualified.', 'products': [{'@id': guard.package_purl(p)} for p in manifest['packages']]}]} @@ -86,7 +86,7 @@ def fixture(absence=False): manifest['qualification']['sha256'] = guard.sha(encoded(qualification)) files = {name: encoded(value) for name, value in [('manifest.json', manifest), ('qualification.json', qualification), ('source-inputs.lock.json', source), ('install.json', install), ('verify.json', verified), ('openvex.json', vex)]} - files['bundle-image.txt'] = ('ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@' + DIGEST + '\n').encode() + files['bundle-image.txt'] = ('ghcr.io/pasturestack/ubuntu-security-backports:v0.1.1@' + DIGEST + '\n').encode() sbom = {'bomFormat': 'CycloneDX', 'components': [{'name': p['name'], 'version': p['version'], 'purl': guard.package_purl(p)} for p in manifest['packages'] if p['name'] in selected]} return files, sbom @@ -110,6 +110,23 @@ def refresh_bindings(files): class ConsumerEvidenceTests(unittest.TestCase): + def test_brotli_is_bound_to_selected_exact_package_and_producer(self): + files, sbom = fixture() + package = next(p for p in json.loads(files['manifest.json'])['packages'] if p['name'] == 'libbrotli1') + for name in ['install.json', 'verify.json']: + change(files, name, lambda d: d['packages'].append('libbrotli1')) + sbom['components'].append({'name': package['name'], 'version': package['version'], + 'purl': guard.package_purl(package)}) + guard.verify(files, checksums(files), sbom, DIGEST, producer_checksums(files)) + altered = copy.deepcopy(sbom) + altered['components'][-1]['version'] = '1.2.0-3build1' + with self.assertRaises(ValueError): + guard.verify(files, checksums(files), altered, DIGEST, producer_checksums(files)) + original = producer_checksums(files) + change(files, 'manifest.json', lambda d: next(p for p in d['packages'] if p['name'] == 'libbrotli1')['archive'].update(sha256='f' * 64)) + with self.assertRaisesRegex(ValueError, 'immutable OCI producer'): + guard.verify(files, checksums(files), sbom, DIGEST, original) + def test_source_native_fixture_requires_no_debs_and_uses_production_generator(self): with tempfile.TemporaryDirectory(prefix='glibc-vex-source-unit-') as directory: work = Path(directory); target = work / 'fixture' @@ -126,7 +143,7 @@ def test_exact_source_bound_glibc_absence_is_distinct_from_fixed(self): files, sbom = fixture(absence=True); original = copy.deepcopy((files, sbom)) result = guard.verify(files, checksums(files), sbom, DIGEST, producer_checksums(files)) self.assertEqual(result['qualifiedCodeAbsentVexPairs'], 3) - self.assertEqual(result['qualifiedFixedVexPairs'], 12) + self.assertEqual(result['qualifiedFixedVexPairs'], 13) self.assertEqual((files, sbom), original) self.assertEqual(len(guard.FILES), 7) diff --git a/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py b/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py index 48a316f5bc..d5b89f1a2f 100644 --- a/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py +++ b/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py @@ -18,7 +18,7 @@ 'glibc': {'libc6', 'libc-bin', 'libc-gconv-modules-extra'}, 'rust-coreutils': {'rust-coreutils'}, 'jq': {'jq', 'libjq1'}, 'p11-kit': {'libp11-kit0', 'p11-kit', 'p11-kit-modules'}, - 'libxrender': {'libxrender1'}, + 'libxrender': {'libxrender1'}, 'brotli': {'libbrotli1'}, } FILES = ('manifest.json', 'qualification.json', 'openvex.json', 'source-inputs.lock.json', 'install.json', 'verify.json', 'bundle-image.txt') @@ -170,7 +170,7 @@ def validate_glibc_vex_semantics(receipt, vex_sha, source_lock_sha): def verify(files, checksums, sbom, expected_digest, bundle_checksums): require(DIGEST.fullmatch(expected_digest) is not None, 'Expected bundle digest is not immutable') - expected_image = 'ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@' + expected_digest + expected_image = 'ghcr.io/pasturestack/ubuntu-security-backports:v0.1.1@' + expected_digest require(files['bundle-image.txt'].decode().strip() == expected_image, 'Consumer bundle identity mismatch') pins = {} for line in checksums.decode().splitlines(): @@ -194,7 +194,7 @@ def verify(files, checksums, sbom, expected_digest, bundle_checksums): manifest, qualification = docs['manifest.json'], docs['qualification.json'] for name in ('manifest.json', 'qualification.json', 'source-inputs.lock.json'): item = docs[name] - require(item['schemaVersion'] == 1 and item['bundleVersion'] == '0.1.0' + require(item['schemaVersion'] == 1 and item['bundleVersion'] == '0.1.1' and item['platform'] == PLATFORM, 'Unexpected qualified bundle schema/platform') require(manifest['qualification'] == {'path': 'qualification.json', 'sha256': sha(files['qualification.json'])}, 'Manifest qualification binding mismatch') @@ -203,7 +203,7 @@ def verify(files, checksums, sbom, expected_digest, bundle_checksums): 'Qualification/source-lock binding mismatch') families = {f['name']: f for f in manifest['families']} qualified = {f['name']: f for f in qualification['families']} - require(len(manifest['families']) == len(qualification['families']) == 6 + require(len(manifest['families']) == len(qualification['families']) == 7 and set(families) == set(qualified) == set(FAMILIES), 'Invalid qualified family set') for name, family in families.items(): q = qualified[name] @@ -212,7 +212,7 @@ def verify(files, checksums, sbom, expected_digest, bundle_checksums): and SHA256.fullmatch(q['provenanceSha256']) and SHA256.fullmatch(q['nativeInput']['lockSha256']), 'Unqualified family provenance') packages = {p['name']: p for p in manifest['packages']} - require(len(manifest['packages']) == 12 and set(packages) == set().union(*FAMILIES.values()), + require(len(manifest['packages']) == 13 and set(packages) == set().union(*FAMILIES.values()), 'Invalid runtime package catalog') purls = {} for name, package in packages.items(): @@ -227,7 +227,7 @@ def verify(files, checksums, sbom, expected_digest, bundle_checksums): purls[package_purl(package)] = package install, installed = docs['install.json'], docs['verify.json'] require(install['schemaVersion'] == installed['schemaVersion'] == 1 - and install['bundleVersion'] == installed['bundleVersion'] == '0.1.0' + and install['bundleVersion'] == installed['bundleVersion'] == '0.1.1' and install['status'] == 'installed-and-verified' and installed['status'] == 'verified' and install['providerPreserved'] is True and install['configurationPreserved'] is True, 'Installation/installed-verifier did not succeed') From 94fecb687ff6ea6495b8006f31ea3adac442df9c Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sun, 25 Oct 2026 15:59:59 +0800 Subject: [PATCH 5/5] Align native VEX fixture staging with bundle 0.1.1 --- .../ubuntu-backports/bundle/test-trivy-glibc-vex.py | 2 +- .../bundle/test_qualify_native_packages.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py b/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py index 71300b39fc..fbedf482db 100644 --- a/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py +++ b/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py @@ -18,7 +18,7 @@ GUARD_SPEC = importlib.util.spec_from_file_location('semantic_metadata_guard', HERE / 'verify-consumer-evidence.py') GUARD = importlib.util.module_from_spec(GUARD_SPEC) GUARD_SPEC.loader.exec_module(GUARD) -WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.0' +WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.1' FIXTURE = WORK / 'glibc-vex-semantic' CVE = GUARD.GLIBC_NSCD_CVE VERSION = GUARD.GLIBC_NSCD_VERSION 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 e1f37dc7d2..09403d2555 100644 --- a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py +++ b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py @@ -44,6 +44,13 @@ def pcre2_api(stage): class Boundaries(unittest.TestCase): + def test_semantic_fixture_uses_same_bundle_work_root(self): + spec = importlib.util.spec_from_file_location('semantic_paths', HERE / 'test-trivy-glibc-vex.py') + semantic = importlib.util.module_from_spec(spec) + spec.loader.exec_module(semantic) + self.assertEqual(semantic.WORK, Q.WORK) + self.assertEqual(semantic.FIXTURE, Q.WORK / 'glibc-vex-semantic') + def test_brotli_qualification_replays_owning_receipts_and_three_library_contract(self): xml = '' + '' * 12 + '' python = 'Ran 135 tests in 0.1s\n\nOK\n'