diff --git a/.gitattributes b/.gitattributes
index 83edcddae1..b5d6c83458 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,6 +4,9 @@
*.patch text eol=lf
Dockerfile* text eol=lf
**/Dockerfile* text eol=lf
+server/security/ubuntu-backports/bundle/*.json text eol=lf
+server/security/ubuntu-backports/bundle/installer/*.go text eol=lf
+server/security/ubuntu-backports/bundle/installer/go.mod text eol=lf
scripts/** text eol=lf
server/bin/** text eol=lf
server/service/** text eol=lf
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index e9330e2381..870670e96e 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -63,6 +63,14 @@ jobs:
shell: bash
run: python3 scripts/test-published-install-gate.py
+ - name: Test the qualified backport bundle contracts
+ if: matrix.language == 'python'
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ python3 -B server/security/ubuntu-backports/bundle/test_qualify_native_packages.py
+ python3 -B server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py
+
- name: Set up the production Go compiler
if: matrix.language == 'go'
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
@@ -123,6 +131,23 @@ jobs:
-ldflags='-s -w -buildid=' \
-o "$RUNNER_TEMP/codeql-go-build/pasturestack-console-broker" .
+ - name: Build and test the static image-build backport installer
+ if: matrix.language == 'go'
+ working-directory: server/security/ubuntu-backports/bundle/installer
+ shell: bash
+ env:
+ CGO_ENABLED: '0'
+ GOOS: linux
+ GOARCH: amd64
+ GOTOOLCHAIN: local
+ GOPROXY: 'off'
+ run: |
+ set -Eeuo pipefail
+ test "$(go env GOVERSION)" = 'go1.27.3'
+ go test -mod=readonly ./...
+ go build -mod=readonly -trimpath -buildvcs=false -ldflags='-s -w -buildid=' \
+ -o "$RUNNER_TEMP/codeql-go-build/pasturestack-install-ubuntu-backports" .
+
- name: Compile Java sources with the production Engine libraries
if: matrix.language == 'java-kotlin'
shell: bash
diff --git a/.github/workflows/publish-ubuntu-security-backports.yml b/.github/workflows/publish-ubuntu-security-backports.yml
new file mode 100644
index 0000000000..2c4c505ff7
--- /dev/null
+++ b/.github/workflows/publish-ubuntu-security-backports.yml
@@ -0,0 +1,218 @@
+name: Publish Ubuntu Security Backport Inputs
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/publish-ubuntu-security-backports.yml'
+ - 'server/security/ubuntu-backports/bundle/**'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ubuntu-security-backports-v0.1.0-${{ github.event_name }}
+ cancel-in-progress: false
+
+jobs:
+ checks:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ fetch-depth: 0
+ - name: Check the source lock and qualifier boundaries
+ run: |
+ set -Eeuo pipefail
+ python3 -B server/security/ubuntu-backports/bundle/test_qualify_native_packages.py
+ python3 -B server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --check-lock
+
+ publish:
+ needs: checks
+ if: github.event_name == 'workflow_dispatch' && github.repository == 'PastureStack/server' && github.ref == 'refs/heads/main'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 90
+ permissions:
+ contents: read
+ actions: read
+ packages: write
+ id-token: write
+ attestations: write
+ artifact-metadata: write
+ env:
+ IMAGE: ghcr.io/pasturestack/ubuntu-security-backports
+ TAG: v0.1.0
+ steps:
+ - name: Check out the exact protected publisher source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.sha }}
+ path: source
+ persist-credentials: false
+ fetch-depth: 0
+
+ - name: Require all native package inputs before building
+ id: inputs
+ working-directory: source
+ run: |
+ set -Eeuo pipefail
+ qualifier=server/security/ubuntu-backports/bundle/qualify-native-packages.py
+ python3 -B "$qualifier" --require-ready
+ printf 'work=%s\n' "$(python3 -B "$qualifier" --print-work-root)" >>"$GITHUB_OUTPUT"
+ python3 - <<'PY' >>"$GITHUB_OUTPUT"
+ import json
+ from pathlib import Path
+ lock = json.loads(Path('server/security/ubuntu-backports/bundle/source-inputs.lock.json').read_text())
+ print('pcre2-source=' + lock['pcre2']['sourceCommit'])
+ PY
+
+ - name: Authenticate the existing Server owner package namespace
+ env:
+ REGISTRY_TOKEN: ${{ github.token }}
+ run: printf '%s' "$REGISTRY_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin
+
+ - name: Refuse an existing tag or an ambiguous registry response
+ env:
+ REGISTRY_TOKEN: ${{ github.token }}
+ working-directory: source
+ run: python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --check-new-tag
+
+ - name: Download and bind the reviewed native artifacts
+ working-directory: source
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --fetch
+
+ - name: Check out the released Server 551 PCRE2 producer
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ steps.inputs.outputs.pcre2-source }}
+ path: pcre2-source
+ persist-credentials: false
+
+ - name: Build only the released PCRE2 producer and static installer
+ working-directory: source
+ env:
+ WORK: ${{ steps.inputs.outputs.work }}
+ run: |
+ set -Eeuo pipefail
+ 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 \
+ 2>&1 | tee "$WORK/publisher/installer-build.log"
+
+ - name: Qualify package bytes, native receipts, payload metadata, SBOM and exact-package VEX
+ working-directory: source
+ env:
+ WORK: ${{ steps.inputs.outputs.work }}
+ run: |
+ set -Eeuo pipefail
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --qualify \
+ 2>&1 | tee "$WORK/publisher/qualification.log"
+
+ - name: Require real installation in both isolated Ubuntu provider fixtures
+ working-directory: source
+ env:
+ WORK: ${{ steps.inputs.outputs.work }}
+ run: |
+ set -Eeuo pipefail
+ for provider in gnu uutils; do
+ docker buildx build --platform linux/amd64 --progress plain --no-cache \
+ --file server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile \
+ --target evidence --build-arg "PROVIDER=$provider" \
+ --build-context "qualified=$WORK/context" \
+ --output "type=local,dest=$WORK/consumer-$provider" server/security/ubuntu-backports/bundle/installer \
+ 2>&1 | tee "$WORK/publisher/consumer-$provider.log"
+ done
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --record-consumer-tests \
+ 2>&1 | tee "$WORK/publisher/consumer-qualification.log"
+
+ - name: Publish the new build-only OCI data artifact
+ id: image
+ working-directory: source
+ env:
+ WORK: ${{ steps.inputs.outputs.work }}
+ SOURCE_SHA: ${{ github.sha }}
+ REGISTRY_TOKEN: ${{ github.token }}
+ run: |
+ set -Eeuo pipefail
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --check-new-tag
+ docker buildx build --platform linux/amd64 --progress plain --provenance=false \
+ --file server/security/ubuntu-backports/bundle/Dockerfile.bundle --target bundle \
+ --build-arg "SOURCE_SHA=$SOURCE_SHA" --tag "$IMAGE:$TAG" \
+ --metadata-file "$WORK/publisher/image-metadata.json" --push "$WORK/context" \
+ 2>&1 | tee "$WORK/publisher/publish.log"
+ digest="$(python3 -c 'import json,os; print(json.load(open(os.environ["WORK"] + "/publisher/image-metadata.json"))["containerimage.digest"])')"
+ [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]
+ printf 'digest=%s\n' "$digest" >>"$GITHUB_OUTPUT"
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --verify-published-manifest
+
+ - name: Attest image provenance with the Server workflow identity
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
+ with:
+ subject-name: ghcr.io/pasturestack/ubuntu-security-backports
+ subject-digest: ${{ steps.image.outputs.digest }}
+ push-to-registry: true
+
+ - name: Attest payload SBOM
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
+ with:
+ subject-name: ghcr.io/pasturestack/ubuntu-security-backports
+ subject-digest: ${{ steps.image.outputs.digest }}
+ sbom-path: ${{ steps.inputs.outputs.work }}/context/out/sbom.cdx.json
+ push-to-registry: true
+
+ - name: Require public anonymous pull and exact OCI data readback
+ working-directory: source
+ env:
+ WORK: ${{ steps.inputs.outputs.work }}
+ IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
+ run: |
+ set -Eeuo pipefail
+ mkdir "$WORK/anonymous-docker" "$WORK/readback"
+ printf '{"auths":{}}\n' >"$WORK/anonymous-docker/config.json"
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --verify-anonymous-manifest
+ docker --config "$WORK/anonymous-docker" pull "$IMAGE@$IMAGE_DIGEST"
+ container="$(docker create --entrypoint /unused "$IMAGE@$IMAGE_DIGEST")"
+ trap 'docker rm "$container" >/dev/null' EXIT
+ docker cp "$container:/out" "$WORK/readback/out"
+ docker cp "$container:/bundle/install-existing-backports" "$WORK/readback/bundle-installer"
+ python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --verify-readback \
+ 2>&1 | tee "$WORK/publisher/anonymous-readback.log"
+ printf 'Package inputs published: %s@%s\nConsumer installation, rootfs scan and product acceptance remain required.\n' \
+ "$IMAGE" "$IMAGE_DIGEST" >>"$GITHUB_STEP_SUMMARY"
+
+ - name: Clear registry credentials
+ if: always()
+ run: docker logout ghcr.io
+
+ - name: Retain exact qualified package inputs and publication evidence
+ 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 }}
+ path: |
+ ${{ steps.inputs.outputs.work }}/context/out/
+ ${{ steps.inputs.outputs.work }}/publisher/
+ ${{ steps.inputs.outputs.work }}/consumer-gnu/
+ ${{ steps.inputs.outputs.work }}/consumer-uutils/
+ ${{ steps.inputs.outputs.work }}/inputs/*-api.json
+ if-no-files-found: warn
+ retention-days: 30
diff --git a/.github/workflows/ubuntu-backport-native.yml b/.github/workflows/ubuntu-backport-native.yml
new file mode 100644
index 0000000000..4cc7b72937
--- /dev/null
+++ b/.github/workflows/ubuntu-backport-native.yml
@@ -0,0 +1,135 @@
+name: Ubuntu Backport Native Candidate
+
+on:
+ pull_request:
+ # Initial candidates get a native run; subsequent fixes select only affected
+ # modules through workflow_dispatch so unchanged native PASS is not rebuilt.
+ types: [opened, reopened]
+ paths:
+ - '.github/workflows/ubuntu-backport-native.yml'
+ - 'server/security/ubuntu-backports/Dockerfile.native-ci'
+ - 'server/security/ubuntu-backports/build-pcre2-backport.py'
+ - 'server/security/ubuntu-backports/glibc/**'
+ - 'server/security/ubuntu-backports/rust-coreutils/**'
+ - 'server/security/ubuntu-backports/jq/**'
+ - 'server/security/ubuntu-backports/p11-kit/**'
+ - 'server/security/ubuntu-backports/libxrender/**'
+ - 'server/security/ubuntu-backports/source_helpers.py'
+ workflow_dispatch:
+ inputs:
+ module:
+ description: Native candidate to build and fully test
+ required: true
+ type: choice
+ default: all
+ options: [all, glibc, rust-coreutils, jq, p11-kit, libxrender]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ubuntu-backport-native-${{ github.event.pull_request.number || github.ref }}-${{ inputs.module || 'all' }}
+ cancel-in-progress: true
+
+jobs:
+ native:
+ name: Native (${{ matrix.module }})
+ runs-on: ubuntu-24.04
+ timeout-minutes: 240
+ strategy:
+ fail-fast: false
+ matrix:
+ module: ${{ fromJSON(inputs.module && inputs.module != 'all' && format('["{0}"]', inputs.module) || '["glibc","rust-coreutils","jq","p11-kit","libxrender"]') }}
+ env:
+ MODULE: ${{ matrix.module }}
+ SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ BUILD_JOBS: ${{ matrix.module == 'glibc' && '4' || '2' }}
+ MIN_FREE_GIB: ${{ matrix.module == 'glibc' && '20' || matrix.module == 'rust-coreutils' && '24' || '8' }}
+
+ steps:
+ - name: Check out the exact candidate head
+ uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
+ with:
+ repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+ persist-credentials: false
+
+ - name: Verify source identity and available disk
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ [[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
+ test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
+ evidence="$RUNNER_TEMP/ubuntu-backport-$MODULE"
+ mkdir -p "$evidence"
+ printf '%s\n' "$SOURCE_SHA" >"$evidence/source-sha.txt"
+ git rev-parse 'HEAD^{tree}' >"$evidence/source-tree.txt"
+ docker_root="$(docker info --format '{{.DockerRootDir}}')"
+ df -Pk "$RUNNER_TEMP" "$docker_root" | tee "$evidence/disk-space.txt"
+ available_kib="$(df -Pk "$docker_root" | awk 'NR == 2 {print $4}')"
+ required_kib=$((MIN_FREE_GIB * 1024 * 1024))
+ if (( available_kib < required_kib )); then
+ printf 'Native %s requires at least %s GiB free Docker workspace; available=%s KiB\n' \
+ "$MODULE" "$MIN_FREE_GIB" "$available_kib" >&2
+ exit 1
+ fi
+
+ - name: Validate the module lock and focused tests
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ module_dir="server/security/ubuntu-backports/$MODULE"
+ test_name="test_${MODULE//-/_}_backport.py"
+ helper="build-$MODULE-backport.py"
+ if [[ "$MODULE" == jq ]]; then test_name=test_jq_release.py; helper=build-jq-release.py; fi
+ evidence="$RUNNER_TEMP/ubuntu-backport-$MODULE"
+ python3 -B "$module_dir/$test_name" -v 2>&1 | tee "$evidence/module-tests.log"
+ python3 -B "$module_dir/$helper" --validate-lock \
+ 2>&1 | tee "$evidence/lock-validation.log"
+
+ - name: Build and export the isolated native evidence
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ evidence="$RUNNER_TEMP/ubuntu-backport-$MODULE"
+ docker buildx version
+ docker buildx build \
+ --platform linux/amd64 \
+ --file server/security/ubuntu-backports/Dockerfile.native-ci \
+ --build-arg "MODULE=$MODULE" \
+ --build-arg "BUILD_JOBS=$BUILD_JOBS" \
+ --build-arg "SOURCE_SHA=$SOURCE_SHA" \
+ --progress plain \
+ --provenance=false \
+ --metadata-file "$evidence/buildkit-metadata.json" \
+ --output "type=local,dest=$evidence/export" \
+ server/security/ubuntu-backports \
+ 2>&1 | tee "$evidence/docker-build.log"
+
+ - name: Enforce the original native exit status
+ if: always()
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ evidence="$RUNNER_TEMP/ubuntu-backport-$MODULE"
+ python3 - "$evidence/export" <<'PY'
+ import json, os, pathlib
+ directory = pathlib.Path(__import__('sys').argv[1])
+ result = json.loads((directory / 'result.json').read_text())
+ assert result['module'] == os.environ['MODULE'], 'Wrong module receipt'
+ assert result['sourceSha'] == os.environ['SOURCE_SHA'], 'Wrong candidate head'
+ assert result['exitCode'] == result['helperExitCode'] == result['logExitCode'] == 0, result
+ assert result['status'] == 'passed', result
+ assert (directory / 'package/provenance.json').is_file(), 'Missing owning-test provenance'
+ print(json.dumps(result, indent=2))
+ PY
+
+ - name: Retain native candidate evidence even on failure
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: ubuntu-native-${{ matrix.module }}-${{ github.event.pull_request.head.sha || github.sha }}-${{ github.run_id }}
+ path: ${{ runner.temp }}/ubuntu-backport-${{ matrix.module }}
+ if-no-files-found: warn
+ retention-days: 14
+ compression-level: 9
diff --git a/server/security/ubuntu-backports/Dockerfile.native-ci b/server/security/ubuntu-backports/Dockerfile.native-ci
new file mode 100644
index 0000000000..7dff462a67
--- /dev/null
+++ b/server/security/ubuntu-backports/Dockerfile.native-ci
@@ -0,0 +1,192 @@
+# syntax=docker/dockerfile:1.7
+# This image is a disposable native build fixture. Only /out/artifacts is exported.
+FROM ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b AS native
+ARG MODULE=glibc
+ARG BUILD_JOBS=2
+ARG SOURCE_SHA
+ENV DEBIAN_FRONTEND=noninteractive PYTHONDONTWRITEBYTECODE=1
+SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
+
+# The original Ubuntu image has no CA bundle. Pin the bootstrap package before
+# using the signed, fixed-date Ubuntu archive; TLS verification remains enabled.
+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
+mkdir -p /tmp/ca-bootstrap /etc/ssl/certs
+dpkg-deb --extract /tmp/ca-certificates.deb /tmp/ca-bootstrap
+while IFS= read -r certificate; do
+ cat "$certificate"
+ printf '\n'
+done < <(find /tmp/ca-bootstrap/usr/share/ca-certificates -type f -name '*.crt' | LC_ALL=C sort) >/etc/ssl/certs/ca-certificates.crt
+rm -f /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources
+cat >/etc/apt/sources.list.d/pasturestack-snapshot.sources <<'SOURCES'
+Types: deb deb-src
+URIs: https://snapshot.ubuntu.com/ubuntu/20261023T000000Z
+Suites: resolute resolute-updates resolute-security
+Components: main universe restricted multiverse
+Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
+Snapshot: no
+Check-Valid-Until: no
+SOURCES
+cat >/etc/apt/apt.conf.d/80pasturestack <<'APT'
+Acquire::https::CaInfo "/etc/ssl/certs/ca-certificates.crt";
+Acquire::https::Verify-Peer "true";
+Acquire::https::Verify-Host "true";
+Acquire::Retries "3";
+APT
+apt-get update
+apt-get install --no-install-recommends -y \
+ build-essential debhelper dpkg-dev fakeroot patch quilt binutils python3 \
+ ca-certificates gzip sudo util-linux
+SH
+
+# The narrow context contains only these reviewed modules and their shared helper.
+COPY . /modules/
+RUN <<'SH'
+set -euo pipefail
+helper="build-$MODULE-backport.py"
+if [[ "$MODULE" == jq ]]; then helper=build-jq-release.py; fi
+python3 -B "/modules/$MODULE/$helper" --validate-lock
+apt-cache showsrc --only-source "$MODULE" >/tmp/native-source-index
+export MODULE
+python3 - <<'PY'
+import json, os, pathlib
+module = os.environ['MODULE']
+lock = json.loads(pathlib.Path(f'/modules/{module}/{module}.lock.json').read_text())
+records = []
+for paragraph in pathlib.Path('/tmp/native-source-index').read_text().split('\n\n'):
+ fields, key = {}, None
+ for line in paragraph.splitlines():
+ if line.startswith((' ', '\t')) and key:
+ fields[key] += '\n' + line.strip()
+ elif ':' in line:
+ key, value = line.split(':', 1)
+ fields[key] = value.strip()
+ if fields.get('Package') == module and fields.get('Version') == lock['ubuntuSourceVersion']:
+ records.append(fields)
+assert records, 'Pinned source version absent from the fixed snapshot'
+for fields in records:
+ checksums = {name: (sha, int(size)) for sha, size, name in
+ (line.split() for line in fields['Checksums-Sha256'].splitlines() if line.strip())}
+ for item in lock['sourceFiles']:
+ assert checksums.get(item['name']) == (item['sha256'], item['size']), 'Snapshot/lock source mismatch'
+pathlib.Path('/tmp/native-source-version').write_text(lock['ubuntuSourceVersion'])
+PY
+version="$(cat /tmp/native-source-version)"
+if [[ "$MODULE" == glibc ]]; then
+ apt-get build-dep --no-install-recommends --arch-only --build-profiles=nobiarch,noudeb -y "glibc=$version"
+else
+ apt-get build-dep --no-install-recommends --arch-only -y "$MODULE=$version"
+fi
+if [[ "$MODULE" == libxrender ]]; then
+ # CI-only private display for the actual QueryFormats regression. Never
+ # connect to a host display or add Xvfb to a product's runtime dependencies.
+ apt-get install --no-install-recommends -y xvfb xauth
+fi
+if [[ "$MODULE" == p11-kit ]]; then
+ # The upstream RPC server suite uses p11tool; without it that test is
+ # skipped. This is a disposable CI dependency, never a runtime addition.
+ apt-get install --no-install-recommends -y gnutls-bin
+fi
+useradd --create-home --user-group --uid 10001 --shell /bin/bash builder
+if [[ "$MODULE" == rust-coreutils ]]; then
+ # Only the helper's bounded credential subprocess uses sudo. No host mount
+ # or persistent host user/group edit is available in this disposable build.
+ printf 'builder ALL=(root) NOPASSWD: /usr/bin/setpriv *\n' >/etc/sudoers.d/native-ci-setpriv
+ chmod 0440 /etc/sudoers.d/native-ci-setpriv
+ visudo --check --file=/etc/sudoers.d/native-ci-setpriv
+fi
+install -d -o builder -g builder -m 0755 /work /out /out/artifacts
+cp /tmp/native-source-index /out/artifacts/snapshot-source-index.txt
+cp /tmp/native-source-version /out/artifacts/snapshot-source-version.txt
+chown builder:builder /out/artifacts/snapshot-source-*.txt
+SH
+
+USER builder
+WORKDIR /work
+RUN <<'SH'
+set -euo pipefail
+[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
+[[ "$BUILD_JOBS" == 2 || "$BUILD_JOBS" == 4 ]]
+test "$(id -u)" != 0
+export MODULE SOURCE_SHA
+if [[ "$MODULE" == rust-coreutils ]]; then
+ # Truthful upstream CI semantics for this GitHub-hosted disposable build.
+ # Other modules and all runtime packages retain their original environment.
+ export CI=true
+fi
+df -Pk /work /out >/out/artifacts/container-disk-space.txt
+dpkg-query --show --showformat='${binary:Package}\t${Version}\n' | LC_ALL=C sort \
+ >/out/artifacts/builder-packages.tsv
+test_name="test_${MODULE//-/_}_backport.py"
+helper="build-$MODULE-backport.py"
+if [[ "$MODULE" == jq ]]; then test_name=test_jq_release.py; helper=build-jq-release.py; fi
+python3 -B "/modules/$MODULE/$test_name" -v
+
+# Defer the unchanged helper's exit solely to export its failure evidence.
+# result.json is enforced by the workflow's always-running gate; a failed helper
+# or failed log stream can never produce a passing native candidate job.
+set +e
+python3 -B "/modules/$MODULE/$helper" \
+ --output-dir /out/artifacts/package --work-dir /work/native --jobs "$BUILD_JOBS" \
+ 2>&1 | tee /out/artifacts/native-console.log
+codes=("${PIPESTATUS[@]}")
+set -e
+export HELPER_EXIT_CODE="${codes[0]}" LOG_EXIT_CODE="${codes[1]}"
+python3 - <<'PY'
+import hashlib, json, os, pathlib
+module = os.environ['MODULE']
+helper, log = int(os.environ['HELPER_EXIT_CODE']), int(os.environ['LOG_EXIT_CODE'])
+lock = pathlib.Path(f'/modules/{module}/{module}.lock.json')
+result = {'schemaVersion': 1, 'module': module, 'sourceSha': os.environ['SOURCE_SHA'],
+ 'helperExitCode': helper, 'logExitCode': log, 'exitCode': helper or log,
+ 'status': 'passed' if helper == log == 0 else 'failed',
+ 'sourceLockSha256': hashlib.sha256(lock.read_bytes()).hexdigest(),
+ 'publicationPerformed': False, 'deploymentPerformed': False}
+pathlib.Path('/out/artifacts/result.json').write_text(json.dumps(result, indent=2) + '\n')
+PY
+
+# Retain only logs/configuration/results; never export source, Cargo target trees,
+# the container rootfs, cores, or the complete native build directory.
+mkdir /out/artifacts/diagnostics
+for name in build.log strict-tests.log; do
+ if [[ -f "/work/native/$name" ]]; then
+ gzip -c "/work/native/$name" >"/out/artifacts/diagnostics/$name.gz"
+ fi
+done
+if [[ "$MODULE" == glibc ]]; then
+ for name in tests.sum configparms config.make; do
+ path="/work/native/glibc-source/build-tree/amd64-libc/$name"
+ if [[ -f "$path" ]]; then
+ gzip -c "$path" >"/out/artifacts/diagnostics/$name.gz"
+ fi
+ done
+ # If a post-build evidence check fails, retain only the three bounded
+ # unqualified runtime packages, not the source tree or debug packages.
+ # result.json and the workflow gate remain failed; these are diagnostics,
+ # never a qualified package output or a publication input.
+ if [[ "$HELPER_EXIT_CODE" != 0 ]]; then
+ python3 - <<'PY'
+import hashlib, json, pathlib, shutil
+lock = json.loads(pathlib.Path('/modules/glibc/glibc.lock.json').read_text())
+source = pathlib.Path('/work/native')
+out = pathlib.Path('/out/artifacts/diagnostics/unqualified-packages')
+out.mkdir()
+files = []
+for name in ('libc6', 'libc-bin', 'libc-gconv-modules-extra'):
+ matches = list(source.glob(name + '_' + lock['customVersion'] + '_amd64.deb'))
+ if len(matches) == 1:
+ file = matches[0]
+ shutil.copyfile(file, out / file.name)
+ files.append({'name': file.name, 'sha256': hashlib.sha256(file.read_bytes()).hexdigest()})
+(out / 'UNQUALIFIED.json').write_text(json.dumps({'status': 'FAILED_NATIVE_DIAGNOSTICS_ONLY',
+ 'publicationAllowed': False, 'files': files}, indent=2) + '\n')
+PY
+ fi
+fi
+gzip /out/artifacts/native-console.log
+SH
+
+FROM scratch AS artifacts
+COPY --from=native /out/artifacts/ /
diff --git a/server/security/ubuntu-backports/README.md b/server/security/ubuntu-backports/README.md
new file mode 100644
index 0000000000..a18ae9503c
--- /dev/null
+++ b/server/security/ubuntu-backports/README.md
@@ -0,0 +1,53 @@
+# Official source security backports
+
+These are first-party rebuild tools, not Canonical packages or a statement that
+any running product is fixed. Each module retains the original Ubuntu packaging,
+pins official inputs by SHA-256, and requires its owning tests and extracted
+package/ABI checks before exporting evidence. Native build helpers export packages;
+the separate build-only bundle installs only qualified, already-present packages
+into disposable consumer image builds.
+
+`ubuntu-backport-native.yml` builds disposable Ubuntu26 amd64 candidates without
+host mounts, publishing credentials or deployment rights. Initial pull requests
+run native validation; after changes, explicitly dispatch only affected modules.
+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
+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.
+
+Before a product consumes a candidate, require the exact native result,
+provenance, source lock, package hashes, ABI and affected runtime smoke. Any
+remaining CVE or unknown architecture/reachability condition stays tracked.
+Never overwrite a released package/image tag or patch a running container.
+
+The attempted tar CVE-2026-18477 backport was rejected: its original cyclic-rename
+regression loses three restored files on the Ubuntu baseline. Source application
+alone was insufficient. Neither it nor the incomplete CVE-2026-18508 backport is
+offered here; a complete compatible official fix is still required.
+
+## Shared consumer inputs
+
+`bundle/` binds the successful native job, source commit, source lock, package
+bytes, licensing and regression evidence for each family. The separately
+versioned `ubuntu-security-backports` OCI package is a build input, not a service,
+runtime base image or Catalog entry. Consumers pin its immutable digest and
+mount it only in their final runtime build stage, after their existing Ubuntu
+security updates. Their compiler/build stages retain their original packages.
+
+The static Go installer selects only packages already installed in that rootfs.
+It checks archive and payload hashes, dependencies, ABI, package integrity and
+provider/configuration preservation before accepting an installation. It must
+not add an absent Rust, development or other runtime package, change a coreutils
+provider, invoke apt, use force flags or patch a running container. Legitimate
+Ubuntu image documentation exclusions are recorded and restored only for the
+selected package paths; runtime exclusions or integrity differences fail.
+
+Publishing requires real disposable consumer installs with both GNU and uutils
+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.
diff --git a/server/security/ubuntu-backports/bundle/Dockerfile.bundle b/server/security/ubuntu-backports/bundle/Dockerfile.bundle
new file mode 100644
index 0000000000..0d449a809c
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/Dockerfile.bundle
@@ -0,0 +1,25 @@
+# syntax=docker/dockerfile:1.7
+# This is a build-only data artifact. It has no runtime entrypoint.
+FROM golang:1.27.3-bookworm@sha256:972b4110ba0f47539a975c31e52c64765e1ff19f2d74469ae4a595c6676e80a5 AS installer_build
+WORKDIR /src
+COPY installer/ ./
+RUN set -eux; \
+ test "$(go env GOVERSION)" = go1.27.3; \
+ export CGO_ENABLED=0 GOTOOLCHAIN=local GOOS=linux GOARCH=amd64 GOPROXY=off GOSUMDB=off; \
+ test -z "$(gofmt -l .)"; \
+ go test -mod=readonly ./...; \
+ go build -mod=readonly -trimpath -buildvcs=false -ldflags='-s -w -buildid=' \
+ -o /out/install-existing-backports .
+
+FROM scratch AS installer_export
+COPY --from=installer_build /out/install-existing-backports /install-existing-backports
+
+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.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."
+COPY out/ /out/
+COPY out/install-existing-backports /bundle/install-existing-backports
diff --git a/server/security/ubuntu-backports/bundle/installer/README.md b/server/security/ubuntu-backports/bundle/installer/README.md
new file mode 100644
index 0000000000..310d9b5f7a
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/README.md
@@ -0,0 +1,59 @@
+The installer is a Go 1.27.3 standard-library-only Linux amd64 binary. Build it
+with `CGO_ENABLED=0 GOTOOLCHAIN=local GOOS=linux GOARCH=amd64`, run `go test ./...`,
+and build with `go build -trimpath -ldflags='-s -w'`. The publisher places the same
+binary at `/bundle/install-existing-backports` and `/out/install-existing-backports`.
+It is a build tool, not a runtime dependency.
+
+In a disposable Ubuntu 26.04 amd64 Docker build, mount the immutable bundle's
+`/out` read-only at `/opt/pasturestack/ubuntu-security-backports`, then execute
+`/opt/pasturestack/ubuntu-security-backports/install-existing-backports --install-in-docker-build`. Use a build mount
+or remove the copied binary and bundle in that same `RUN`. The final runtime
+needs only its existing dpkg utilities; Python, readelf, nm and a Go toolchain
+are not added. `--verify-installed` performs the installed checks without an
+installation. No root/manifest/path override is accepted.
+
+Manifest v1 fixes Ubuntu 26.04 amd64, bundle 0.1.0, six families and twelve
+runtime packages: PCRE2's two libraries; libc6, libc-bin and
+libc-gconv-modules-extra; rust-coreutils; jq and libjq1; p11-kit,
+p11-kit-modules and libp11-kit0; and libxrender1. Archives, complete non-directory
+payload receipts, source provenance and qualification are SHA256-bound. The
+installer independently reads each selected archive's control and tar stream,
+checks every file/link and ELF identity, and preserves existing SONAME, NEEDED
+and exported symbols before installation. The publisher qualifies native owning
+test evidence; consumer installation and product acceptance remain separate.
+
+Only packages already installed in the consumer are selected. The package set
+must remain identical; no Rust package or provider is added to a GNU-only image.
+Exact-version dependencies from installed companion packages (including dev
+packages) reject an upgrade if a matching companion is absent from the bundle.
+The dependency dry run and actual batch installation use fixed dpkg options,
+without apt, force options or dependency overrides. Original provider package
+metadata, provider command paths, alternatives, diversions, statoverrides and
+host configuration are compared before/after. Only the generated ld.so.cache
+is excluded from the host-configuration snapshot.
+
+Before installation, dpkg checksum differences or missing runtime files fail.
+The sole permitted pre-existing difference is a missing selected-package
+documentation file proven excluded by the consumer's current dpkg path rules.
+Every such path is enumerated in the JSON receipt. Exact one-shot
+`--path-include=/full/path` options restore selected documentation without
+editing dpkg configuration. Other excluded runtime payload paths fail. After
+installation, every payload file's SHA256/type/mode/uid/gid or literal symlink
+target, every ELF receipt, package metadata, dpkg audit and full `dpkg --verify`
+must match. No post-install difference is ignored.
+
+Tests use temporary rootfs/bundle fixtures and a fake dpkg adapter. They never
+install a package, change the host, run apt, or invoke libc-maintainer scripts.
+
+`consumer-fixture.Dockerfile` adds a separate prepublication acceptance gate on
+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
+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.
diff --git a/server/security/ubuntu-backports/bundle/installer/confined_reads_test.go b/server/security/ubuntu-backports/bundle/installer/confined_reads_test.go
new file mode 100644
index 0000000000..ab97cedf38
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/confined_reads_test.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestConfinedReadsRejectTraversalAndOutsideSymlinkParents(t *testing.T) {
+ root, outside := t.TempDir(), t.TempDir()
+ write(t, outside, "secret", []byte("outside bytes must not be read"))
+ must(t, os.Symlink(outside, filepath.Join(root, "escape")))
+ for _, name := range []string{"../secret", "/secret", "escape/secret"} {
+ t.Run(name, func(t *testing.T) {
+ if _, err := regularRead(root, name, 1024); err == nil {
+ t.Fatal("bundle read escaped root")
+ }
+ if _, b, err := installedFile(root, File{Path: name}); err == nil || len(b) != 0 {
+ t.Fatal("installed file read escaped root")
+ }
+ })
+ }
+}
+
+func TestConfinedReadsPreserveRuntimeLinksAndBundleNoLinks(t *testing.T) {
+ root := t.TempDir()
+ write(t, root, "usr/lib/value", []byte("confined value"))
+ must(t, os.Symlink("lib", filepath.Join(root, "usr/alias")))
+ got, b, err := installedFile(root, File{Path: "usr/alias/value"})
+ must(t, err)
+ if got.SHA256 != digest([]byte("confined value")) || string(b) != "confined value" {
+ t.Fatal("legitimate relative runtime symlink failed")
+ }
+ if _, err := regularRead(root, "usr/alias/value", 1024); err == nil {
+ t.Fatal("bundle symlink parent was accepted")
+ }
+ must(t, os.Symlink("/usr/lib/value", filepath.Join(root, "usr/link")))
+ got, b, err = installedFile(root, File{Path: "usr/link"})
+ must(t, err)
+ if got.Type != "symlink" || got.Target != "/usr/lib/value" || len(b) != 0 {
+ t.Fatal("literal absolute final symlink was followed or changed")
+ }
+}
+
+func TestConfinedOpenRejectsFinalSymlinkAndKeepsBounds(t *testing.T) {
+ root := t.TempDir()
+ write(t, root, "value", []byte("12345"))
+ must(t, os.Symlink("value", filepath.Join(root, "link")))
+ directory, err := os.OpenRoot(root)
+ must(t, err)
+ defer directory.Close()
+ if file, err := openRegularInRoot(directory, "link"); err == nil {
+ file.Close()
+ t.Fatal("final symlink was followed by the confined regular open")
+ }
+ if _, err := regularRead(root, "value", 4); err == nil {
+ t.Fatal("size bound was weakened")
+ }
+ b, err := regularRead(root, "value", 5)
+ must(t, err)
+ if string(b) != "12345" {
+ t.Fatal("bounded legitimate read failed")
+ }
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile b/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile
new file mode 100644
index 0000000000..2a9e6a82a8
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/consumer-fixture.Dockerfile
@@ -0,0 +1,60 @@
+# syntax=docker/dockerfile:1.7
+# Disposable prepublication consumer acceptance; never used as a product image.
+FROM ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b AS consumer
+ARG PROVIDER=gnu
+ENV DEBIAN_FRONTEND=noninteractive
+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 "$PROVIDER" in gnu|uutils) ;; *) 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
+ cat "$certificate"
+ printf '\n'
+done < <(find /tmp/ca-bootstrap/usr/share/ca-certificates -type f -name '*.crt' | LC_ALL=C sort) >/etc/ssl/certs/ca-certificates.crt
+rm -f /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources
+cat >/etc/apt/sources.list.d/pasturestack-snapshot.sources <<'SOURCES'
+Types: deb
+URIs: https://snapshot.ubuntu.com/ubuntu/20261023T000000Z
+Suites: resolute resolute-updates resolute-security
+Components: main universe restricted multiverse
+Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
+Snapshot: no
+Check-Valid-Until: no
+SOURCES
+cat >/etc/apt/apt.conf.d/80pasturestack <<'APT'
+Acquire::https::CaInfo "/etc/ssl/certs/ca-certificates.crt";
+Acquire::https::Verify-Peer "true";
+Acquire::https::Verify-Host "true";
+Acquire::Retries "3";
+APT
+# Exercise an ordinary minimal-image doc filter, without suppressing runtime
+# 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
+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
+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.
+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
+RUN --mount=from=qualified,source=/out,target=/opt/pasturestack/ubuntu-security-backports,ro \
+ /bin/bash /consumer-smoke.sh "$PROVIDER"
+
+FROM scratch AS evidence
+COPY --from=consumer /evidence/ /
diff --git a/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh b/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh
new file mode 100644
index 0000000000..a960fbcb43
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/consumer-smoke.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+# Only run by consumer-fixture.Dockerfile in an isolated image build.
+set -euo pipefail
+export LC_ALL=C
+provider="${1:?provider required}"
+case "$provider" in gnu|uutils) ;; *) exit 2 ;; esac
+test "$#" -eq 1
+readonly data=/opt/pasturestack/ubuntu-security-backports
+readonly evidence=/evidence
+mkdir -p "$evidence"
+exec > >(tee "$evidence/console.log") 2>&1
+
+installed() {
+ test "$(dpkg-query --show --showformat='${db:Status-Status}' "$1" 2>/dev/null)" = installed
+}
+absent() {
+ if installed "$1"; then
+ printf 'Unexpected installed package: %s\n' "$1" >&2
+ return 1
+ fi
+}
+package_set() {
+ dpkg-query --show --showformat='${binary:Package}\t${Architecture}\t${db:Status-Status}\n' |
+ awk -F '\t' '$3 == "installed" { print $1 "\t" $2 }' | sort
+}
+provider_state() {
+ dpkg-query --show --showformat='${binary:Package}\t${Version}\t${Architecture}\t${db:Status-Status}\n' \
+ "coreutils-from-$provider" gnu-coreutils
+ stat --format='%F:%a:%u:%g' /usr/bin/ls /usr/bin/id
+ for command in /usr/bin/ls /usr/bin/id; do
+ if test -L "$command"; then readlink "$command"; else sha256sum "$command"; fi
+ done
+}
+installed "coreutils-from-$provider"
+installed gnu-coreutils
+if [[ "$provider" == gnu ]]; then
+ absent rust-coreutils
+ absent coreutils-from-uutils
+ expected=11
+else
+ installed rust-coreutils
+ absent coreutils-from-gnu
+ expected=12
+fi
+for package in libc6-dev libc-dev-bin libpcre2-dev libjq-dev libp11-kit-dev libxrender-dev; do
+ absent "$package"
+done
+input_manifest_sha="$(sha256sum "$data/manifest.json" | awk '{print $1}')"
+package_set >"$evidence/packages-before.tsv"
+provider_state >"$evidence/provider-before.tsv"
+sha256sum /etc/ld.so.cache >"$evidence/ld-so-cache-before.sha256"
+
+# The only installer invocation which mutates this disposable consumer rootfs.
+"$data/install-existing-backports" --install-in-docker-build >"$evidence/install.json"
+"$data/install-existing-backports" --verify-installed >"$evidence/verify.json"
+cat "$evidence/install.json" "$evidence/verify.json"
+jq -e --argjson expected "$expected" '.status == "installed-and-verified" and .providerPreserved and .configurationPreserved and (.packages | length) == $expected and (.beforeMissingDocumentation | length) > 0 and (.exactPathIncludes | length) > 0' "$evidence/install.json" >/dev/null
+jq -e '.status == "verified"' "$evidence/verify.json" >/dev/null
+cmp <(jq -c '.packages|sort' "$evidence/install.json") <(jq -c '.packages|sort' "$evidence/verify.json")
+if [[ "$provider" == gnu ]]; then
+ absent rust-coreutils
+ jq -e '.packages | index("rust-coreutils") == null' "$evidence/install.json" >/dev/null
+else
+ installed rust-coreutils
+ jq -e '.packages | index("rust-coreutils") != null' "$evidence/install.json" >/dev/null
+fi
+package_set >"$evidence/packages-after.tsv"
+provider_state >"$evidence/provider-after.tsv"
+cmp "$evidence/packages-before.tsv" "$evidence/packages-after.tsv"
+cmp "$evidence/provider-before.tsv" "$evidence/provider-after.tsv"
+while IFS= read -r path; do
+ case "$path" in /usr/share/doc/*|/usr/share/man/*|/usr/share/info/*) ;; *) exit 1 ;; esac
+ test -e "$path" || test -L "$path"
+done < <(jq -r '.exactPathIncludes[]' "$evidence/install.json")
+test -z "$(dpkg --audit)"
+
+# Exercise the installed loader/cache and programs from the same qualified
+# archives. This does not repeat the upstream compiler/owning suites.
+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
+ grep -F "$soname " "$evidence/ldconfig.txt"
+done
+getconf GNU_LIBC_VERSION | tee "$evidence/glibc-version.txt" | grep -Fx 'glibc 2.43'
+/lib64/ld-linux-x86-64.so.2 --verify /usr/bin/jq
+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'
+test "$(id -u)" = 0
+test "$(id -g)" = 0
+id -G >"$evidence/id-groups.txt"
+p11-kit list-modules >"$evidence/p11-kit-modules.txt"
+printf 'a123\n' | grep -P '^a\d{3}$'
+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","id","p11-kit","grep","getent"]}' >"$evidence/result.json"
+cat "$evidence/result.json"
diff --git a/server/security/ubuntu-backports/bundle/installer/go.mod b/server/security/ubuntu-backports/bundle/installer/go.mod
new file mode 100644
index 0000000000..3ecd223d2d
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/go.mod
@@ -0,0 +1,3 @@
+module pasturestack.local/ubuntu-security-backports-installer
+
+go 1.27.3
diff --git a/server/security/ubuntu-backports/bundle/installer/installer.go b/server/security/ubuntu-backports/bundle/installer/installer.go
new file mode 100644
index 0000000000..9ac34f8d46
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/installer.go
@@ -0,0 +1,498 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+type Installed struct{ Name, Version, Architecture, Source, SourceVersion, Status, Depends, PreDepends string }
+type Runner interface {
+ Run(string, ...string) ([]byte, error)
+}
+type Installer struct {
+ bundle, root string
+ run Runner
+}
+type Receipt struct {
+ SchemaVersion int `json:"schemaVersion"`
+ BundleVersion string `json:"bundleVersion"`
+ Status string `json:"status"`
+ Packages []string `json:"packages"`
+ BeforeMissingDocumentation []string `json:"beforeMissingDocumentation"`
+ ExactPathIncludes []string `json:"exactPathIncludes"`
+ ProviderPreserved bool `json:"providerPreserved,omitempty"`
+ ConfigurationPreserved bool `json:"configurationPreserved,omitempty"`
+}
+
+func parseInventory(b []byte) (map[string]Installed, error) {
+ result := map[string]Installed{}
+ for _, line := range strings.Split(strings.TrimSuffix(string(b), "\n"), "\n") {
+ if line == "" {
+ continue
+ }
+ fields := strings.Split(line, "\t")
+ if len(fields) != 8 {
+ return nil, fmt.Errorf("malformed dpkg inventory")
+ }
+ name := strings.SplitN(fields[0], ":", 2)[0]
+ if fields[5] != "installed" {
+ if fields[5] == "not-installed" || fields[5] == "config-files" {
+ continue
+ }
+ return nil, fmt.Errorf("package not fully configured: %s (%s)", fields[0], fields[5])
+ }
+ if _, duplicate := result[name]; duplicate {
+ return nil, fmt.Errorf("multiarch duplicate package: %s", name)
+ }
+ result[name] = Installed{name, fields[1], fields[2], fields[3], fields[4], fields[5], fields[6], fields[7]}
+ }
+ return result, nil
+}
+func (i Installer) inventory() (map[string]Installed, error) {
+ b, err := i.run.Run("/usr/bin/dpkg-query", "--show", "--showformat=${binary:Package}\t${Version}\t${Architecture}\t${source:Package}\t${source:Version}\t${db:Status-Status}\t${Depends}\t${Pre-Depends}\n")
+ if err != nil {
+ return nil, err
+ }
+ return parseInventory(b)
+}
+func (i Installer) platform() error {
+ b, err := os.ReadFile(filepath.Join(i.root, "etc/os-release"))
+ if err != nil {
+ return err
+ }
+ fields := map[string]string{}
+ for _, line := range strings.Split(string(b), "\n") {
+ k, v, ok := strings.Cut(line, "=")
+ if ok {
+ fields[k] = strings.Trim(v, "\"'")
+ }
+ }
+ if fields["ID"] != "ubuntu" || fields["VERSION_ID"] != "26.04" {
+ return fmt.Errorf("only Ubuntu 26.04 rootfs is supported")
+ }
+ b, err = i.run.Run("/usr/bin/dpkg", "--print-architecture")
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(string(b)) != "amd64" {
+ return fmt.Errorf("only native amd64 is supported")
+ }
+ return nil
+}
+func parseControl(b []byte) (map[string]string, error) {
+ result := map[string]string{}
+ last := ""
+ for _, line := range strings.Split(strings.TrimSuffix(string(b), "\n"), "\n") {
+ if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
+ if last == "" {
+ return nil, fmt.Errorf("invalid control continuation")
+ }
+ result[last] += " " + strings.TrimSpace(line)
+ continue
+ }
+ key, value, ok := strings.Cut(line, ":")
+ if !ok || key == "" {
+ return nil, fmt.Errorf("invalid control field")
+ }
+ if _, duplicate := result[key]; duplicate {
+ return nil, fmt.Errorf("duplicate control field")
+ }
+ last = key
+ result[key] = strings.TrimSpace(value)
+ }
+ return result, nil
+}
+func (i Installer) archive(p Package, payload Payload) error {
+ archive := filepath.Join(i.bundle, filepath.FromSlash(p.Archive.Path))
+ b, err := i.run.Run("/usr/bin/dpkg-deb", "--field", archive)
+ if err != nil {
+ return err
+ }
+ c, err := parseControl(b)
+ if err != nil {
+ return err
+ }
+ source := c["Source"]
+ if source == "" && p.Name == p.Source {
+ source = p.Source
+ }
+ if c["Package"] != p.Name || c["Version"] != p.Version || c["Architecture"] != p.Architecture || (source != p.Source && source != p.Source+" ("+p.Version+")") {
+ return fmt.Errorf("archive control identity mismatch: %s", p.Name)
+ }
+ b, err = i.run.Run("/usr/bin/dpkg-deb", "--fsys-tarfile", archive)
+ if err != nil {
+ return err
+ }
+ return validateArchive(b, payload)
+}
+
+var exactDependency = regexp.MustCompile(`(?:^|[,|])\s*([a-z0-9][a-z0-9+.-]*)(?::[a-z0-9-]+)?\s*\(=\s*([^()]+)\)`)
+
+func companions(installed map[string]Installed, selected []Package) error {
+ versions := map[string]string{}
+ for _, p := range selected {
+ versions[p.Name] = p.Version
+ }
+ for name, p := range installed {
+ if _, upgraded := versions[name]; upgraded {
+ continue
+ }
+ for _, match := range exactDependency.FindAllStringSubmatch(p.Depends+","+p.PreDepends, -1) {
+ if target, upgraded := versions[match[1]]; upgraded && target != strings.TrimSpace(match[2]) {
+ return fmt.Errorf("installed companion %s requires %s (= %s); matching companion archive is absent", name, match[1], strings.TrimSpace(match[2]))
+ }
+ }
+ }
+ return nil
+}
+
+type pathRule struct {
+ pattern string
+ include bool
+}
+
+func (i Installer) pathRules() ([]pathRule, error) {
+ paths := []string{filepath.Join(i.root, "etc/dpkg/dpkg.cfg")}
+ extra, err := filepath.Glob(filepath.Join(i.root, "etc/dpkg/dpkg.cfg.d/*"))
+ if err != nil {
+ return nil, err
+ }
+ sort.Strings(extra)
+ paths = append(paths, extra...)
+ rules := []pathRule{}
+ for _, p := range paths {
+ info, err := os.Lstat(p)
+ if os.IsNotExist(err) {
+ continue
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !info.Mode().IsRegular() {
+ return nil, fmt.Errorf("dpkg configuration is not a regular file: %s", p)
+ }
+ b, err := os.ReadFile(p)
+ if err != nil {
+ return nil, err
+ }
+ for _, line := range strings.Split(string(b), "\n") {
+ line = strings.TrimSpace(strings.SplitN(line, "#", 2)[0])
+ line = strings.TrimPrefix(line, "--")
+ for _, kind := range []string{"path-exclude", "path-include"} {
+ if strings.HasPrefix(line, kind+"=") || strings.HasPrefix(line, kind+" ") {
+ pattern := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(line, kind), "="))
+ if !strings.HasPrefix(pattern, "/") || strings.ContainsAny(pattern, "\x00\r\n") {
+ return nil, fmt.Errorf("invalid dpkg path rule")
+ }
+ rules = append(rules, pathRule{pattern, kind == "path-include"})
+ }
+ }
+ }
+ }
+ return rules, nil
+}
+
+// dpkg's pathname filters use '*' across '/' (unlike filepath.Match).
+func excluded(name string, rules []pathRule) bool {
+ exclude := false
+ for _, rule := range rules {
+ pattern := "^"
+ for _, c := range rule.pattern {
+ switch c {
+ case '*':
+ pattern += ".*"
+ case '?':
+ pattern += "."
+ default:
+ pattern += regexp.QuoteMeta(string(c))
+ }
+ }
+ pattern += "$"
+ if regexp.MustCompile(pattern).MatchString("/" + name) {
+ exclude = !rule.include
+ }
+ }
+ return exclude
+}
+func documentation(name, pkg string) bool {
+ return name == "usr/share/doc/"+pkg || strings.HasPrefix(name, "usr/share/doc/"+pkg+"/") || strings.HasPrefix(name, "usr/share/man/") || strings.HasPrefix(name, "usr/share/info/")
+}
+
+var missingLine = regexp.MustCompile(`^missing\s+(/\S+)$`)
+
+func beforeVerify(output []byte, p Package, payload Payload, rules []pathRule) ([]string, error) {
+ allowed := map[string]bool{}
+ for _, f := range payload.Files {
+ if documentation(f.Path, p.Name) && excluded(f.Path, rules) {
+ allowed["/"+f.Path] = true
+ }
+ }
+ missing := []string{}
+ for _, line := range strings.Split(strings.TrimSuffix(string(output), "\n"), "\n") {
+ if line == "" {
+ continue
+ }
+ match := missingLine.FindStringSubmatch(line)
+ if match == nil || !allowed[match[1]] {
+ return nil, fmt.Errorf("pre-existing package integrity difference: %s: %s", p.Name, line)
+ }
+ missing = append(missing, match[1])
+ }
+ return unique(missing), nil
+}
+func (i Installer) audit() error {
+ b, err := i.run.Run("/usr/bin/dpkg", "--audit")
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(string(b)) != "" {
+ return fmt.Errorf("dpkg audit is not clean: %s", b)
+ }
+ return nil
+}
+
+// Config files and provider selection are immutable across installation. Only
+// ld.so.cache is generated by the required libc trigger; it is not host config.
+func (i Installer) snapshot(installed map[string]Installed) (map[string]string, error) {
+ result := map[string]string{}
+ directory, err := os.OpenRoot(i.root)
+ if err != nil {
+ return nil, err
+ }
+ defer directory.Close()
+ add := func(name string) error {
+ if name == "etc/ld.so.cache" {
+ return nil
+ }
+ if !safeRelative(name) {
+ return fmt.Errorf("unsafe snapshot path %q", name)
+ }
+ info, err := directory.Lstat(name)
+ if os.IsNotExist(err) {
+ result[name] = "absent"
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+ f, _, err := installedFile(i.root, File{Path: name})
+ if err != nil {
+ return err
+ }
+ result[name] = fmt.Sprintf("%s:%04o:%d:%d:%s:%s", f.Type, f.Mode, f.UID, f.GID, f.SHA256, f.Target)
+ return nil
+ }
+ for _, dir := range []string{"etc", "var/lib/dpkg/alternatives"} {
+ err := filepath.WalkDir(filepath.Join(i.root, dir), func(p string, d os.DirEntry, err error) error {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ if d.IsDir() {
+ return nil
+ }
+ name, err := filepath.Rel(i.root, p)
+ if err != nil {
+ return err
+ }
+ return add(filepath.ToSlash(name))
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+ for _, name := range []string{"var/lib/dpkg/diversions", "var/lib/dpkg/statoverride"} {
+ if err := add(name); err != nil {
+ return nil, err
+ }
+ }
+ for _, provider := range []string{"coreutils-from-uutils", "coreutils-from-gnu", "gnu-coreutils", "coreutils"} {
+ p, ok := installed[provider]
+ if !ok {
+ continue
+ }
+ result["provider-package:"+provider] = fmt.Sprintf("%#v", p)
+ b, err := i.run.Run("/usr/bin/dpkg-query", "--listfiles", provider)
+ if err != nil {
+ return nil, err
+ }
+ for _, name := range strings.Split(string(b), "\n") {
+ if strings.HasPrefix(name, "/usr/bin/") || strings.HasPrefix(name, "/bin/") {
+ if err := add(strings.TrimPrefix(name, "/")); err != nil {
+ return nil, err
+ }
+ }
+ }
+ }
+ return result, nil
+}
+func unchangedInventory(before, after map[string]Installed, selected []Package) error {
+ if len(before) != len(after) {
+ return fmt.Errorf("installation changed package set")
+ }
+ chosen := map[string]Package{}
+ for _, p := range selected {
+ chosen[p.Name] = p
+ }
+ for name, old := range before {
+ now, exists := after[name]
+ if !exists {
+ return fmt.Errorf("package removed: %s", name)
+ }
+ if p, ok := chosen[name]; ok {
+ if now.Version != p.Version || now.Architecture != p.Architecture || now.Source != p.Source || now.SourceVersion != p.Version || now.Status != "installed" {
+ return fmt.Errorf("installed package identity mismatch: %s", name)
+ }
+ } else if old != now {
+ return fmt.Errorf("unselected package changed: %s", name)
+ }
+ }
+ return nil
+}
+func (i Installer) Execute(install bool) (Receipt, error) {
+ r := Receipt{SchemaVersion: 1, BundleVersion: "0.1.0", Packages: []string{}, BeforeMissingDocumentation: []string{}, ExactPathIncludes: []string{}}
+ if err := i.platform(); err != nil {
+ return r, err
+ }
+ m, payloads, err := loadManifest(i.bundle)
+ if err != nil {
+ return r, err
+ }
+ installed, err := i.inventory()
+ if err != nil {
+ return r, err
+ }
+ if err = i.audit(); err != nil {
+ return r, err
+ }
+ selected := []Package{}
+ for _, p := range m.Packages {
+ old, exists := installed[p.Name]
+ if !exists {
+ continue
+ }
+ if old.Architecture != "amd64" || old.Source != p.Source {
+ return r, fmt.Errorf("unexpected installed source/architecture: %s", p.Name)
+ }
+ if !install && old.Version != p.Version {
+ return r, fmt.Errorf("installed version is not qualified: %s", p.Name)
+ }
+ if _, err = i.run.Run("/usr/bin/dpkg", "--compare-versions", old.Version, "le", p.Version); err != nil {
+ return r, fmt.Errorf("refuse downgrade: %s", p.Name)
+ }
+ if err = i.archive(p, payloads[p.Name]); err != nil {
+ return r, err
+ }
+ selected = append(selected, p)
+ r.Packages = append(r.Packages, p.Name)
+ }
+ if len(selected) == 0 {
+ return r, fmt.Errorf("no existing qualified packages selected")
+ }
+ if err = companions(installed, selected); err != nil {
+ return r, err
+ }
+ if !install {
+ for _, p := range selected {
+ if err = verifyPayload(i.root, payloads[p.Name]); err != nil {
+ return r, err
+ }
+ out, e := i.run.Run("/usr/bin/dpkg", "--verify", p.Name)
+ if e != nil || len(out) != 0 {
+ return r, fmt.Errorf("post-install dpkg verification failed: %s: %s", p.Name, out)
+ }
+ }
+ r.Status = "verified"
+ return r, nil
+ }
+ rules, err := i.pathRules()
+ if err != nil {
+ return r, err
+ }
+ includes := []string{}
+ for _, p := range selected {
+ out, e := i.run.Run("/usr/bin/dpkg", "--verify", p.Name)
+ if e != nil && len(out) == 0 {
+ return r, e
+ }
+ missing, err := beforeVerify(out, p, payloads[p.Name], rules)
+ if err != nil {
+ return r, err
+ }
+ r.BeforeMissingDocumentation = append(r.BeforeMissingDocumentation, missing...)
+ for _, f := range payloads[p.Name].Files {
+ if excluded(f.Path, rules) {
+ if !documentation(f.Path, p.Name) {
+ return r, fmt.Errorf("dpkg excludes a runtime payload path: %s", f.Path)
+ }
+ includes = append(includes, "/"+f.Path)
+ }
+ }
+ if err = checkABI(i.root, payloads[p.Name]); err != nil {
+ return r, err
+ }
+ }
+ r.BeforeMissingDocumentation = unique(r.BeforeMissingDocumentation)
+ r.ExactPathIncludes = unique(includes)
+ before, err := i.snapshot(installed)
+ if err != nil {
+ return r, err
+ }
+ archives := []string{}
+ for _, p := range selected {
+ archives = append(archives, filepath.Join(i.bundle, filepath.FromSlash(p.Archive.Path)))
+ }
+ dry := append([]string{"--no-act", "--install"}, archives...)
+ if _, err = i.run.Run("/usr/bin/dpkg", dry...); err != nil {
+ return r, fmt.Errorf("dependency preflight: %w", err)
+ }
+ args := []string{}
+ for _, p := range r.ExactPathIncludes {
+ args = append(args, "--path-include="+p)
+ }
+ args = append(args, "--install")
+ args = append(args, archives...)
+ if _, err = i.run.Run("/usr/bin/dpkg", args...); err != nil {
+ return r, fmt.Errorf("batch installation: %w", err)
+ }
+ after, err := i.inventory()
+ if err != nil {
+ return r, err
+ }
+ if err = unchangedInventory(installed, after, selected); err != nil {
+ return r, err
+ }
+ if err = i.audit(); err != nil {
+ return r, err
+ }
+ now, err := i.snapshot(after)
+ if err != nil {
+ return r, err
+ }
+ if !reflect.DeepEqual(before, now) {
+ return r, fmt.Errorf("provider or host configuration changed")
+ }
+ for _, p := range selected {
+ if err = verifyPayload(i.root, payloads[p.Name]); err != nil {
+ return r, err
+ }
+ out, e := i.run.Run("/usr/bin/dpkg", "--verify", p.Name)
+ if e != nil || len(out) != 0 {
+ return r, fmt.Errorf("post-install dpkg verification failed: %s: %s", p.Name, out)
+ }
+ }
+ r.Status = "installed-and-verified"
+ r.ProviderPreserved = true
+ r.ConfigurationPreserved = true
+ return r, nil
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/installer_test.go b/server/security/ubuntu-backports/bundle/installer/installer_test.go
new file mode 100644
index 0000000000..f40c6dd7b3
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/installer_test.go
@@ -0,0 +1,420 @@
+package main
+
+import (
+ "archive/tar"
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+ "testing"
+)
+
+type fakeRunner struct {
+ t *testing.T
+ root, bundle string
+ installed map[string]Installed
+ packages map[string]Package
+ payloads map[string]Payload
+ tars map[string][]byte
+ contents map[string][]byte
+ beforeDiff, afterDiff map[string]string
+ commands [][]string
+ installedBatch []string
+ mutate func()
+ dryFail bool
+}
+
+func (f *fakeRunner) Run(binary string, args ...string) ([]byte, error) {
+ f.commands = append(f.commands, append([]string{binary}, args...))
+ if binary == "/usr/bin/dpkg-query" && args[0] == "--show" {
+ names := []string{}
+ for n := range f.installed {
+ names = append(names, n)
+ }
+ sort.Strings(names)
+ var b strings.Builder
+ for _, n := range names {
+ p := f.installed[n]
+ fmt.Fprintf(&b, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", p.Name, p.Version, p.Architecture, p.Source, p.SourceVersion, p.Status, p.Depends, p.PreDepends)
+ }
+ return []byte(b.String()), nil
+ }
+ if binary == "/usr/bin/dpkg-query" && args[0] == "--listfiles" {
+ return []byte("/usr/bin/ls\n"), nil
+ }
+ if binary == "/usr/bin/dpkg-deb" {
+ name := ""
+ for n, p := range f.packages {
+ if filepath.Join(f.bundle, filepath.FromSlash(p.Archive.Path)) == args[1] {
+ name = n
+ }
+ }
+ if name == "" {
+ return nil, errors.New("unexpected archive")
+ }
+ p := f.packages[name]
+ if args[0] == "--field" {
+ return []byte(fmt.Sprintf("Package: %s\nVersion: %s\nArchitecture: amd64\nSource: %s\n", name, p.Version, p.Source)), nil
+ }
+ if args[0] == "--fsys-tarfile" {
+ return f.tars[name], nil
+ }
+ }
+ if binary == "/usr/bin/dpkg" {
+ switch args[0] {
+ case "--print-architecture":
+ return []byte("amd64\n"), nil
+ case "--audit":
+ return nil, nil
+ case "--compare-versions":
+ if args[1] > args[3] {
+ return nil, errors.New("newer installed version")
+ }
+ return nil, nil
+ case "--verify":
+ if f.installedBatch == nil {
+ return []byte(f.beforeDiff[args[1]]), nil
+ }
+ return []byte(f.afterDiff[args[1]]), nil
+ case "--no-act":
+ if f.dryFail {
+ return nil, errors.New("dependency unsatisfied")
+ }
+ return nil, nil
+ }
+ installAt := -1
+ for n, a := range args {
+ if a == "--install" {
+ installAt = n
+ }
+ if strings.Contains(a, "force") {
+ f.t.Fatal("force flag used")
+ }
+ }
+ if installAt >= 0 {
+ if f.installedBatch != nil {
+ f.t.Fatal("more than one actual install")
+ }
+ f.installedBatch = []string{}
+ for _, archive := range args[installAt+1:] {
+ name := ""
+ for n, p := range f.packages {
+ if filepath.Join(f.bundle, filepath.FromSlash(p.Archive.Path)) == archive {
+ name = n
+ }
+ }
+ if name == "" {
+ f.t.Fatal("unrecognized install path")
+ }
+ if _, exists := f.installed[name]; !exists {
+ f.t.Fatal("added a package")
+ }
+ p := f.packages[name]
+ f.installed[name] = Installed{Name: name, Version: p.Version, Architecture: "amd64", Source: p.Source, SourceVersion: p.Version, Status: "installed"}
+ f.installedBatch = append(f.installedBatch, name)
+ for _, file := range f.payloads[name].Files {
+ target := filepath.Join(f.root, filepath.FromSlash(file.Path))
+ must(f.t, os.MkdirAll(filepath.Dir(target), 0755))
+ must(f.t, os.WriteFile(target, f.contents[file.Path], os.FileMode(file.Mode)))
+ }
+ }
+ if f.mutate != nil {
+ f.mutate()
+ }
+ return nil, nil
+ }
+ }
+ return nil, fmt.Errorf("unexpected command: %s %v", binary, args)
+}
+func must(t *testing.T, err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+func write(t *testing.T, root, name string, b []byte) {
+ t.Helper()
+ p := filepath.Join(root, filepath.FromSlash(name))
+ must(t, os.MkdirAll(filepath.Dir(p), 0755))
+ must(t, os.WriteFile(p, b, 0644))
+}
+func jsonFile(t *testing.T, root, name string, value any) Ref {
+ t.Helper()
+ b, err := json.Marshal(value)
+ must(t, err)
+ write(t, root, name, b)
+ return Ref{name, digest(b)}
+}
+func fixture(t *testing.T, names ...string) (Installer, *fakeRunner) {
+ t.Helper()
+ root := t.TempDir()
+ bundle := t.TempDir()
+ write(t, root, "etc/os-release", []byte("ID=ubuntu\nVERSION_ID=\"26.04\"\n"))
+ write(t, root, "etc/dpkg/dpkg.cfg.d/docker", []byte("path-exclude=/usr/share/doc/*\n"))
+ 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{}}
+ 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
+ keys := []string{}
+ for family := range families {
+ keys = append(keys, family)
+ }
+ sort.Strings(keys)
+ for _, family := range keys {
+ lockSHA := digest([]byte(family))
+ prov := jsonFile(t, bundle, "provenance/"+family+"/provenance.json", map[string]string{"sourceLockSha256": lockSHA})
+ m.Families = append(m.Families, Family{family, family, "2.0", prov})
+ qf := QualifiedFamily{Name: family, Status: "qualified", ProvenanceSHA256: prov.SHA256}
+ qf.NativeInput.LockSHA256 = lockSHA
+ q.Families = append(q.Families, qf)
+ for _, name := range families[family] {
+ p := Package{Name: name, Family: family, Version: "2.0", Architecture: "amd64", Source: family}
+ payload := Payload{SchemaVersion: 1, Package: name, Version: p.Version, Architecture: "amd64", Source: family, Files: []File{}, ELF: []ELF{}}
+ var tb bytes.Buffer
+ tw := tar.NewWriter(&tb)
+ for _, filename := range []string{"usr/bin/fixture-" + name, "usr/share/doc/" + name + "/README"} {
+ content := []byte("qualified-" + name + "-" + filename)
+ file := File{Path: filename, Type: "file", SHA256: digest(content), Mode: 0644, UID: os.Geteuid(), GID: os.Getegid()}
+ payload.Files = append(payload.Files, file)
+ f.contents[filename] = content
+ must(t, tw.WriteHeader(&tar.Header{Name: "./" + filename, Mode: 0644, Uid: file.UID, Gid: file.GID, Size: int64(len(content)), Typeflag: tar.TypeReg}))
+ _, err := tw.Write(content)
+ must(t, err)
+ }
+ must(t, tw.Close())
+ f.tars[name] = tb.Bytes()
+ p.Archive = Ref{"debs/" + name + ".deb", digest([]byte("archive-" + name))}
+ write(t, bundle, p.Archive.Path, []byte("archive-"+name))
+ p.Payload = jsonFile(t, bundle, "payload/"+name+".json", payload)
+ m.Packages = append(m.Packages, p)
+ f.packages[name] = p
+ f.payloads[name] = payload
+ }
+ }
+ m.Qualification = jsonFile(t, bundle, "qualification.json", q)
+ jsonFile(t, bundle, "manifest.json", m)
+ for _, name := range names {
+ source := name
+ if p, ok := f.packages[name]; ok {
+ source = p.Source
+ }
+ f.installed[name] = Installed{Name: name, Version: "1.0", Architecture: "amd64", Source: source, SourceVersion: "1.0", Status: "installed"}
+ if p, ok := f.payloads[name]; ok {
+ write(t, root, p.Files[0].Path, []byte("original-"+name))
+ f.beforeDiff[name] = "missing /usr/share/doc/" + name + "/README\n"
+ }
+ }
+ return Installer{bundle: bundle, root: root, run: f}, f
+}
+func TestExistingOnlyGNUConsumer(t *testing.T) {
+ i, f := fixture(t, "libc6", "coreutils-from-gnu", "gnu-coreutils")
+ r, err := i.Execute(true)
+ must(t, err)
+ if !reflect.DeepEqual(f.installedBatch, []string{"libc6"}) || !r.ProviderPreserved || !r.ConfigurationPreserved {
+ t.Fatalf("wrong batch/receipt: %#v", r)
+ }
+ if _, exists := f.installed["rust-coreutils"]; exists {
+ t.Fatal("Rust was added to a GNU consumer")
+ }
+ if !reflect.DeepEqual(r.BeforeMissingDocumentation, []string{"/usr/share/doc/libc6/README"}) || !reflect.DeepEqual(r.ExactPathIncludes, r.BeforeMissingDocumentation) {
+ t.Fatal("missing docs were not explicitly restored")
+ }
+ for _, cmd := range f.commands {
+ if strings.Contains(strings.Join(cmd, " "), "--install") && cmd[1] != "--no-act" {
+ if !strings.Contains(strings.Join(cmd, " "), "--path-include=/usr/share/doc/libc6/README") {
+ t.Fatal("exact path include missing")
+ }
+ }
+ }
+}
+func TestUutilsProviderPreserved(t *testing.T) {
+ i, f := fixture(t, "rust-coreutils", "coreutils-from-uutils", "gnu-coreutils")
+ p := f.installed["coreutils-from-uutils"]
+ p.PreDepends = "rust-coreutils (>> 0.1.0+git20250813), gnu-coreutils"
+ f.installed[p.Name] = p
+ _, err := i.Execute(true)
+ must(t, err)
+ if !reflect.DeepEqual(f.installedBatch, []string{"rust-coreutils"}) {
+ t.Fatal("provider was installed")
+ }
+}
+func TestRefuseMissingExactCompanion(t *testing.T) {
+ i, f := fixture(t, "libc6", "libc6-dev")
+ p := f.installed["libc6-dev"]
+ p.Depends = "libc6 (= 1.0)"
+ f.installed[p.Name] = p
+ _, err := i.Execute(true)
+ if err == nil || !strings.Contains(err.Error(), "companion") {
+ t.Fatalf("expected companion refusal, got %v", err)
+ }
+ if f.installedBatch != nil {
+ t.Fatal("installed despite missing companion")
+ }
+}
+func TestTamperedArchiveStopsBeforeDpkg(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ write(t, i.bundle, f.packages["jq"].Archive.Path, []byte("tampered unselected input"))
+ _, err := i.Execute(true)
+ if err == nil || !strings.Contains(err.Error(), "SHA256") {
+ t.Fatalf("expected SHA refusal, got %v", err)
+ }
+ if f.installedBatch != nil {
+ t.Fatal("installed tampered bundle")
+ }
+}
+func TestPreexistingIntegrityFailures(t *testing.T) {
+ for _, diff := range []string{"??5?????? /usr/bin/fixture-libc6\n", "missing /usr/bin/fixture-libc6\n", "missing c /etc/ld.so.conf\n", "missing /usr/share/doc/other/README\n"} {
+ t.Run(strings.TrimSpace(diff), func(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ f.beforeDiff["libc6"] = diff
+ _, err := i.Execute(true)
+ if err == nil {
+ t.Fatal("corruption accepted")
+ }
+ if f.installedBatch != nil {
+ t.Fatal("installation reached")
+ }
+ })
+ }
+}
+func TestMissingDocsRequireActualPathExclusion(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ write(t, i.root, "etc/dpkg/dpkg.cfg.d/docker", []byte("# no exclusions\n"))
+ _, err := i.Execute(true)
+ if err == nil || f.installedBatch != nil {
+ t.Fatal("unexplained missing file accepted")
+ }
+}
+func TestRuntimePathExclusionRejected(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ f.beforeDiff["libc6"] = ""
+ write(t, i.root, "etc/dpkg/dpkg.cfg.d/docker", []byte("path-exclude=/usr/bin/*\n"))
+ _, err := i.Execute(true)
+ if err == nil || !strings.Contains(err.Error(), "runtime payload") || f.installedBatch != nil {
+ t.Fatalf("runtime exclusion not rejected: %v", err)
+ }
+}
+func TestDryRunFailureNoMutation(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ f.dryFail = true
+ _, err := i.Execute(true)
+ if err == nil || f.installedBatch != nil {
+ t.Fatal("failed dependency preflight mutated rootfs")
+ }
+}
+func TestPostIntegrityAndConfigFailures(t *testing.T) {
+ for _, kind := range []string{"dpkg", "payload", "config", "provider"} {
+ t.Run(kind, func(t *testing.T) {
+ i, f := fixture(t, "libc6", "coreutils-from-gnu")
+ switch kind {
+ case "dpkg":
+ f.afterDiff["libc6"] = "??5?????? /usr/bin/fixture-libc6\n"
+ case "payload":
+ f.mutate = func() { write(t, i.root, "usr/bin/fixture-libc6", []byte("bad")) }
+ case "config":
+ f.mutate = func() { write(t, i.root, "etc/os-release", []byte("changed")) }
+ case "provider":
+ f.mutate = func() { write(t, i.root, "usr/bin/ls", []byte("switched")) }
+ }
+ _, err := i.Execute(true)
+ if err == nil {
+ t.Fatal("post-install corruption accepted")
+ }
+ })
+ }
+}
+func TestInstalledVerifierIsReadOnly(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ _, err := i.Execute(true)
+ must(t, err)
+ f.commands = nil
+ _, err = i.Execute(false)
+ must(t, err)
+ for _, cmd := range f.commands {
+ for _, arg := range cmd {
+ if arg == "--install" || arg == "--no-act" {
+ t.Fatal("verifier invoked installation")
+ }
+ }
+ }
+}
+func TestArchiveReceiptAndTraversalRejection(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ p := f.payloads["libc6"]
+ p.Files[0].SHA256 = digest([]byte("wrong"))
+ if err := validateArchive(f.tars["libc6"], p); err == nil {
+ t.Fatal("payload mismatch accepted")
+ }
+ var b bytes.Buffer
+ tw := tar.NewWriter(&b)
+ must(t, tw.WriteHeader(&tar.Header{Name: "../escape", Mode: 0644, Size: 0, Typeflag: tar.TypeReg}))
+ must(t, tw.Close())
+ if _, _, err := inspectTar(b.Bytes()); err == nil {
+ t.Fatal("tar traversal accepted")
+ }
+ _ = i
+}
+func TestCLIHasNoPathOverrides(t *testing.T) {
+ for _, args := range [][]string{{}, {"--lock", "/tmp/x"}, {"--install-in-docker-build", "--verify-installed"}, {"--verify-installed", "/tmp/root"}} {
+ if err := cli(args, &bytes.Buffer{}); err == nil {
+ t.Fatalf("invalid CLI accepted: %v", args)
+ }
+ }
+}
+func TestDowngradeRejected(t *testing.T) {
+ i, f := fixture(t, "libc6")
+ p := f.installed["libc6"]
+ p.Version = "3.0"
+ f.installed[p.Name] = p
+ _, err := i.Execute(true)
+ if err == nil || !strings.Contains(err.Error(), "downgrade") || f.installedBatch != nil {
+ t.Fatalf("downgrade accepted: %v", err)
+ }
+}
+func TestHardlinkAndSymlinkTar(t *testing.T) {
+ var b bytes.Buffer
+ tw := tar.NewWriter(&b)
+ must(t, tw.WriteHeader(&tar.Header{Name: "./usr/lib/a", Mode: 0644, Size: 3, Typeflag: tar.TypeReg}))
+ _, err := tw.Write([]byte("abc"))
+ must(t, err)
+ must(t, tw.WriteHeader(&tar.Header{Name: "./usr/lib/b", Mode: 0644, Typeflag: tar.TypeLink, Linkname: "./usr/lib/a"}))
+ must(t, tw.WriteHeader(&tar.Header{Name: "./usr/lib/c", Mode: 0777, Typeflag: tar.TypeSymlink, Linkname: "/usr/lib/a"}))
+ must(t, tw.Close())
+ files, _, err := inspectTar(b.Bytes())
+ must(t, err)
+ if len(files) != 3 || files[0].SHA256 != files[1].SHA256 || files[2].Target != "/usr/lib/a" {
+ t.Fatal("hardlink/symlink payload receipt wrong")
+ }
+ b.Reset()
+ tw = tar.NewWriter(&b)
+ must(t, tw.WriteHeader(&tar.Header{Name: "./usr/lib/link", Mode: 0777, Typeflag: tar.TypeSymlink, Linkname: "/etc"}))
+ must(t, tw.WriteHeader(&tar.Header{Name: "./usr/lib/link/evil", Mode: 0644, Size: 0, Typeflag: tar.TypeReg}))
+ must(t, tw.Close())
+ if _, _, err := inspectTar(b.Bytes()); err == nil {
+ t.Fatal("archive path below symlink accepted")
+ }
+}
+func TestNativeELFABIRejectsDroppedSymbols(t *testing.T) {
+ const library = "usr/lib/x86_64-linux-gnu/libc.so.6"
+ b, err := os.ReadFile("/" + library)
+ must(t, err)
+ e, err := inspectELF(library, b)
+ must(t, err)
+ if e.SONAME == nil || len(e.DefinedSymbols) == 0 {
+ t.Fatal("native amd64 libc fixture lacks SONAME/exports")
+ }
+ p := Payload{ELF: []ELF{e}}
+ must(t, checkABI("/", p))
+ p.ELF[0].DefinedSymbols = []string{}
+ if err := checkABI("/", p); err == nil || !strings.Contains(err.Error(), "dropped exported symbol") {
+ t.Fatalf("ABI loss was not rejected: %v", err)
+ }
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/main.go b/server/security/ubuntu-backports/bundle/installer/main.go
new file mode 100644
index 0000000000..29908f22dc
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/main.go
@@ -0,0 +1,75 @@
+// install-existing-backports is a build-only, dependency-free verifier/installer.
+// It never invokes a shell, apt, force flags, alternatives, or network tools.
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "runtime"
+)
+
+type boundedBuffer struct {
+ bytes.Buffer
+ limit int
+}
+
+func (b *boundedBuffer) Write(p []byte) (int, error) {
+ if len(p) > b.limit-b.Len() {
+ return 0, fmt.Errorf("command output exceeds bound")
+ }
+ return b.Buffer.Write(p)
+}
+
+type systemRunner struct{}
+
+func (systemRunner) Run(binary string, args ...string) ([]byte, error) {
+ switch binary {
+ case "/usr/bin/dpkg", "/usr/bin/dpkg-query", "/usr/bin/dpkg-deb":
+ default:
+ return nil, fmt.Errorf("command is not allowed")
+ }
+ cmd := exec.Command(binary, args...)
+ cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin", "LC_ALL=C", "LANG=C", "DEBIAN_FRONTEND=noninteractive", "DPKG_COLORS=never", "HOME=/root", "TERM=dumb"}
+ var out = boundedBuffer{limit: 768 << 20}
+ var stderr = boundedBuffer{limit: 64 << 10}
+ cmd.Stdout = &out
+ cmd.Stderr = &stderr
+ cmd.Stdin = nil
+ err := cmd.Run()
+ if err != nil {
+ return out.Bytes(), fmt.Errorf("%s %v: %w; %s", binary, args, err, stderr.String())
+ }
+ return out.Bytes(), nil
+}
+func cli(args []string, stdout io.Writer) error {
+ flags := flag.NewFlagSet("install-existing-backports", flag.ContinueOnError)
+ install := flags.Bool("install-in-docker-build", false, "install the fixed qualified bundle into a disposable Docker build rootfs")
+ verify := flags.Bool("verify-installed", false, "verify existing installed packages against the fixed qualified bundle")
+ if err := flags.Parse(args); err != nil {
+ return err
+ }
+ if flags.NArg() != 0 || *install == *verify {
+ return fmt.Errorf("choose exactly one of --install-in-docker-build or --verify-installed; no path overrides are accepted")
+ }
+ if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" || runtime.Version() != "go1.27.3" || os.Geteuid() != 0 {
+ return fmt.Errorf("requires root inside a Linux amd64 Docker build, compiled with Go 1.27.3")
+ }
+ receipt, err := (Installer{bundle: bundleRoot, root: "/", run: systemRunner{}}).Execute(*install)
+ if err != nil {
+ return err
+ }
+ encoder := json.NewEncoder(stdout)
+ encoder.SetIndent("", " ")
+ return encoder.Encode(receipt)
+}
+func main() {
+ if err := cli(os.Args[1:], os.Stdout); err != nil {
+ fmt.Fprintln(os.Stderr, "BACKPORT_INSTALLATION_FAILED:", err)
+ os.Exit(1)
+ }
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/ownership_linux.go b/server/security/ubuntu-backports/bundle/installer/ownership_linux.go
new file mode 100644
index 0000000000..01769d31ec
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/ownership_linux.go
@@ -0,0 +1,17 @@
+//go:build linux
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "syscall"
+)
+
+func fileOwnership(info os.FileInfo) (uint32, int, int, error) {
+ s, ok := info.Sys().(*syscall.Stat_t)
+ if !ok {
+ return 0, 0, 0, fmt.Errorf("missing Linux stat ownership")
+ }
+ return s.Mode & 07777, int(s.Uid), int(s.Gid), nil
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/ownership_other.go b/server/security/ubuntu-backports/bundle/installer/ownership_other.go
new file mode 100644
index 0000000000..36cd100cea
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/ownership_other.go
@@ -0,0 +1,12 @@
+//go:build !linux
+
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+func fileOwnership(info os.FileInfo) (uint32, int, int, error) {
+ return 0, 0, 0, fmt.Errorf("installer requires Linux")
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/payload.go b/server/security/ubuntu-backports/bundle/installer/payload.go
new file mode 100644
index 0000000000..dcb17b7d0d
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/payload.go
@@ -0,0 +1,283 @@
+package main
+
+import (
+ "archive/tar"
+ "bytes"
+ "debug/elf"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "reflect"
+ "sort"
+ "strings"
+)
+
+func unique(values []string) []string {
+ sort.Strings(values)
+ out := []string{}
+ for _, v := range values {
+ if len(out) == 0 || out[len(out)-1] != v {
+ out = append(out, v)
+ }
+ }
+ return out
+}
+func inspectELF(name string, content []byte) (ELF, error) {
+ e := ELF{Path: name, SHA256: digest(content), ELFClass: 64, Machine: 62, Needed: []string{}, DefinedSymbols: []string{}}
+ f, err := elf.NewFile(bytes.NewReader(content))
+ if err != nil {
+ return e, err
+ }
+ defer f.Close()
+ if f.Class != elf.ELFCLASS64 || f.Machine != elf.EM_X86_64 || f.Data != elf.ELFDATA2LSB {
+ return e, fmt.Errorf("non-amd64 ELF: %s", name)
+ }
+ soname, err := f.DynString(elf.DT_SONAME)
+ if err != nil {
+ return e, err
+ }
+ if len(soname) > 1 {
+ return e, fmt.Errorf("multiple SONAMEs")
+ }
+ if len(soname) == 1 {
+ e.SONAME = &soname[0]
+ }
+ e.Needed, err = f.ImportedLibraries()
+ if err != nil {
+ return e, err
+ }
+ e.Needed = unique(e.Needed)
+ symbols, err := f.DynamicSymbols()
+ if err != nil && err != elf.ErrNoSymbols {
+ return e, err
+ }
+ for _, s := range symbols {
+ if s.Section != elf.SHN_UNDEF && elf.ST_BIND(s.Info) != elf.STB_LOCAL && s.Name != "" {
+ e.DefinedSymbols = append(e.DefinedSymbols, strings.SplitN(s.Name, "@", 2)[0])
+ }
+ }
+ e.DefinedSymbols = unique(e.DefinedSymbols)
+ return e, nil
+}
+
+// Parse the archive without extracting it. All non-directory entries, including
+// hardlinks and symlinks, must exactly match the qualified payload receipt.
+func inspectTar(content []byte) ([]File, []ELF, error) {
+ t := tar.NewReader(bytes.NewReader(content))
+ files := map[string]File{}
+ blobs := map[string][]byte{}
+ links := map[string]string{}
+ dirs := map[string]bool{}
+ var total int64
+ count := 0
+ for {
+ h, err := t.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ name := strings.TrimSuffix(strings.TrimPrefix(h.Name, "./"), "/")
+ if name == "" && h.Typeflag == tar.TypeDir {
+ continue
+ }
+ count++
+ total += h.Size
+ if count > 30000 || h.Size < 0 || total > 1<<30 || !safeRelative(name) || h.Uid < 0 || h.Gid < 0 || h.Mode < 0 || h.Mode > 07777 {
+ return nil, nil, fmt.Errorf("unsafe/unbounded tar entry %q", h.Name)
+ }
+ if _, ok := files[name]; ok || dirs[name] {
+ return nil, nil, fmt.Errorf("duplicate tar path %s", name)
+ }
+ f := File{Path: name, Mode: uint32(h.Mode), UID: h.Uid, GID: h.Gid}
+ switch h.Typeflag {
+ case tar.TypeDir:
+ dirs[name] = true
+ continue
+ case tar.TypeReg, tar.TypeRegA:
+ b, err := io.ReadAll(io.LimitReader(t, 256<<20+1))
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(b) > 256<<20 {
+ return nil, nil, fmt.Errorf("oversize payload file")
+ }
+ f.Type = "file"
+ f.SHA256 = digest(b)
+ blobs[name] = b
+ case tar.TypeSymlink:
+ if h.Linkname == "" || strings.ContainsAny(h.Linkname, "\x00\r\n") {
+ return nil, nil, fmt.Errorf("invalid symlink")
+ }
+ f.Type = "symlink"
+ f.Target = h.Linkname
+ case tar.TypeLink:
+ target := strings.TrimPrefix(h.Linkname, "./")
+ if !safeRelative(target) {
+ return nil, nil, fmt.Errorf("unsafe hardlink")
+ }
+ f.Type = "file"
+ links[name] = target
+ default:
+ return nil, nil, fmt.Errorf("special payload entry: %s", name)
+ }
+ files[name] = f
+ }
+ for len(links) > 0 {
+ progress := false
+ for name, target := range links {
+ if b, ok := blobs[target]; ok {
+ blobs[name] = b
+ f := files[name]
+ f.SHA256 = digest(b)
+ files[name] = f
+ delete(links, name)
+ progress = true
+ }
+ }
+ if !progress {
+ return nil, nil, fmt.Errorf("unresolved/cyclic hardlink")
+ }
+ }
+ result := []File{}
+ elfs := []ELF{}
+ cached := map[string]ELF{}
+ for name, f := range files {
+ for parent := path.Dir(name); parent != "."; parent = path.Dir(parent) {
+ if files[parent].Type == "symlink" {
+ return nil, nil, fmt.Errorf("payload entry below symlink: %s", name)
+ }
+ }
+ result = append(result, f)
+ if b := blobs[name]; f.Type == "file" && bytes.HasPrefix(b, []byte("\x7fELF")) {
+ e, ok := cached[f.SHA256]
+ if !ok {
+ var err error
+ e, err = inspectELF(name, b)
+ if err != nil {
+ return nil, nil, err
+ }
+ cached[f.SHA256] = e
+ }
+ e.Path = name
+ elfs = append(elfs, e)
+ }
+ }
+ sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
+ sort.Slice(elfs, func(i, j int) bool { return elfs[i].Path < elfs[j].Path })
+ return result, elfs, nil
+}
+func validateArchive(content []byte, p Payload) error {
+ files, elfs, err := inspectTar(content)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(files, p.Files) || !reflect.DeepEqual(elfs, p.ELF) {
+ return fmt.Errorf("archive differs from qualified payload/ELF receipt: %s", p.Package)
+ }
+ return nil
+}
+func installedFile(root string, expected File) (File, []byte, error) {
+ f := File{Path: expected.Path}
+ if !safeRelative(expected.Path) {
+ return f, nil, fmt.Errorf("unsafe rootfs path %q", expected.Path)
+ }
+ directory, err := os.OpenRoot(root)
+ if err != nil {
+ return f, nil, err
+ }
+ defer directory.Close()
+ info, err := directory.Lstat(expected.Path)
+ if err != nil {
+ return f, nil, err
+ }
+ f.Mode, f.UID, f.GID, err = fileOwnership(info)
+ if err != nil {
+ return f, nil, err
+ }
+ if info.Mode()&os.ModeSymlink != 0 {
+ f.Type = "symlink"
+ f.Target, err = directory.Readlink(expected.Path)
+ return f, nil, err
+ }
+ if !info.Mode().IsRegular() || info.Size() > 256<<20 {
+ return f, nil, fmt.Errorf("special/unbounded installed file: %s", expected.Path)
+ }
+ f.Type = "file"
+ opened, err := openRegularInRoot(directory, expected.Path)
+ if err != nil {
+ return f, nil, err
+ }
+ defer opened.Close()
+ current, err := opened.Stat()
+ if err != nil {
+ return f, nil, err
+ }
+ if !current.Mode().IsRegular() || current.Size() > 256<<20 || !os.SameFile(info, current) {
+ return f, nil, fmt.Errorf("installed file changed during confined open: %s", expected.Path)
+ }
+ b, err := io.ReadAll(io.LimitReader(opened, (256<<20)+1))
+ if err != nil {
+ return f, nil, err
+ }
+ if len(b) > 256<<20 {
+ return f, nil, fmt.Errorf("installed file grew beyond size bound: %s", expected.Path)
+ }
+ f.SHA256 = digest(b)
+ return f, b, nil
+}
+func verifyPayload(root string, p Payload) error {
+ elfs := map[string]ELF{}
+ for _, e := range p.ELF {
+ elfs[e.Path] = e
+ }
+ for _, expected := range p.Files {
+ got, b, err := installedFile(root, expected)
+ if err != nil {
+ return err
+ }
+ if got != expected {
+ return fmt.Errorf("installed payload metadata/content differs: %s", expected.Path)
+ }
+ if e, ok := elfs[expected.Path]; ok {
+ actual, err := inspectELF(expected.Path, b)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(actual, e) {
+ return fmt.Errorf("installed ELF differs: %s", e.Path)
+ }
+ }
+ }
+ return nil
+}
+func checkABI(root string, p Payload) error {
+ for _, candidate := range p.ELF {
+ if candidate.SONAME == nil {
+ continue
+ }
+ _, b, err := installedFile(root, File{Path: candidate.Path})
+ if err != nil {
+ return err
+ }
+ old, err := inspectELF(candidate.Path, b)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(old.SONAME, candidate.SONAME) || !reflect.DeepEqual(old.Needed, candidate.Needed) {
+ return fmt.Errorf("library SONAME/NEEDED changed: %s", candidate.Path)
+ }
+ available := map[string]bool{}
+ for _, symbol := range candidate.DefinedSymbols {
+ available[symbol] = true
+ }
+ for _, symbol := range old.DefinedSymbols {
+ if !available[symbol] {
+ return fmt.Errorf("library dropped exported symbol %s: %s", symbol, candidate.Path)
+ }
+ }
+ }
+ return nil
+}
diff --git a/server/security/ubuntu-backports/bundle/installer/schema.go b/server/security/ubuntu-backports/bundle/installer/schema.go
new file mode 100644
index 0000000000..748c608bc8
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/installer/schema.go
@@ -0,0 +1,283 @@
+package main
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+const bundleRoot = "/opt/pasturestack/ubuntu-security-backports"
+
+var families = map[string][]string{
+ "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"},
+}
+
+type Platform struct{ Distribution, Release, Architecture string }
+type Ref struct{ Path, SHA256 string }
+type Family struct {
+ Name, Source, Version string
+ Provenance Ref
+}
+type Package struct {
+ Name, Family, Version, Architecture, Source string
+ Archive, Payload Ref
+}
+type Manifest struct {
+ SchemaVersion int
+ BundleVersion string
+ Platform Platform
+ Qualification Ref
+ Families []Family
+ Packages []Package
+}
+type QualifiedFamily struct {
+ Name, Status, ProvenanceSHA256 string
+ NativeInput struct{ LockSHA256 string }
+}
+type Qualification struct {
+ SchemaVersion int
+ BundleVersion, Status, SourceLockSHA256 string
+ Platform Platform
+ Families []QualifiedFamily
+}
+type File struct {
+ Path, Type, SHA256, Target string
+ Mode uint32
+ UID, GID int
+}
+type ELF struct {
+ Path, SHA256 string
+ ELFClass, Machine int
+ SONAME *string
+ Needed, DefinedSymbols []string
+}
+type Payload struct {
+ SchemaVersion int
+ Package, Version, Architecture, Source string
+ Files []File
+ ELF []ELF
+}
+
+var digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
+var versionPattern = regexp.MustCompile(`^[0-9][A-Za-z0-9.+:~\-]*$`)
+
+func digest(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
+func check(ok bool, format string, args ...any) error {
+ if !ok {
+ return fmt.Errorf(format, args...)
+ }
+ return nil
+}
+func safeRelative(p string) bool {
+ return p != "" && p != "." && !strings.ContainsAny(p, "\\\x00\r\n:") &&
+ !strings.HasPrefix(p, "/") && path.Clean(p) == p && p != ".." && !strings.HasPrefix(p, "../")
+}
+
+// The Root capability prevents escape while resolving path components. Keep
+// final-link rejection and identity checks around the confined read-only open.
+func openRegularInRoot(directory *os.Root, name string) (*os.File, error) {
+ before, err := directory.Lstat(name)
+ if err != nil {
+ return nil, err
+ }
+ if !before.Mode().IsRegular() {
+ return nil, fmt.Errorf("not a regular confined file: %s", name)
+ }
+ f, err := directory.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ opened, err := f.Stat()
+ if err != nil {
+ f.Close()
+ return nil, err
+ }
+ after, err := directory.Lstat(name)
+ if err != nil {
+ f.Close()
+ return nil, err
+ }
+ if !after.Mode().IsRegular() || !os.SameFile(before, opened) || !os.SameFile(after, opened) {
+ f.Close()
+ return nil, fmt.Errorf("confined file changed while opening: %s", name)
+ }
+ return f, nil
+}
+
+// Every component is checked without following links, even for SHA-bound files.
+func regularRead(root, name string, limit int64) ([]byte, error) {
+ if !safeRelative(name) {
+ return nil, fmt.Errorf("unsafe bundle path %q", name)
+ }
+ directory, err := os.OpenRoot(root)
+ if err != nil {
+ return nil, err
+ }
+ defer directory.Close()
+ p := ""
+ var checked os.FileInfo
+ parts := strings.Split(name, "/")
+ for n, part := range parts {
+ p = filepath.Join(p, part)
+ info, err := directory.Lstat(p)
+ if err != nil {
+ return nil, err
+ }
+ if info.Mode()&os.ModeSymlink != 0 || (n < len(parts)-1 && !info.IsDir()) {
+ return nil, fmt.Errorf("linked/non-directory parent %s", p)
+ }
+ if n == len(parts)-1 && (!info.Mode().IsRegular() || info.Size() > limit) {
+ return nil, fmt.Errorf("not a bounded regular file: %s", p)
+ }
+ checked = info
+ }
+ f, err := openRegularInRoot(directory, p)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ opened, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+ if !opened.Mode().IsRegular() || opened.Size() > limit || !os.SameFile(checked, opened) {
+ return nil, fmt.Errorf("bundle file changed during confined open: %s", p)
+ }
+ b, err := io.ReadAll(io.LimitReader(f, limit+1))
+ if err != nil {
+ return nil, err
+ }
+ if int64(len(b)) > limit {
+ return nil, fmt.Errorf("file grew beyond size bound: %s", p)
+ }
+ return b, nil
+}
+func boundRead(root string, ref Ref, limit int64) ([]byte, error) {
+ if !digestPattern.MatchString(ref.SHA256) {
+ return nil, fmt.Errorf("invalid SHA256 for %s", ref.Path)
+ }
+ b, err := regularRead(root, ref.Path, limit)
+ if err != nil {
+ return nil, err
+ }
+ if digest(b) != ref.SHA256 {
+ return nil, fmt.Errorf("SHA256 mismatch: %s", ref.Path)
+ }
+ return b, nil
+}
+func decode(b []byte, value any, strict bool) error {
+ d := json.NewDecoder(bytes.NewReader(b))
+ if strict {
+ d.DisallowUnknownFields()
+ }
+ if err := d.Decode(value); err != nil {
+ return err
+ }
+ if err := d.Decode(new(any)); err != io.EOF {
+ return fmt.Errorf("trailing JSON data")
+ }
+ return nil
+}
+func validatePlatform(p Platform) bool { return p == (Platform{"ubuntu", "26.04", "amd64"}) }
+
+func loadManifest(root string) (Manifest, map[string]Payload, error) {
+ var m Manifest
+ b, err := regularRead(root, "manifest.json", 2<<20)
+ if err != nil {
+ return m, nil, err
+ }
+ 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" {
+ return m, nil, fmt.Errorf("unsupported bundle identity/catalog")
+ }
+ qb, err := boundRead(root, m.Qualification, 16<<20)
+ if err != nil {
+ return m, nil, err
+ }
+ var q Qualification // Native owning-test evidence is family-specific; required fields remain mandatory.
+ 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 {
+ return m, nil, fmt.Errorf("bundle is not qualified")
+ }
+ if _, err = boundRead(root, Ref{"source-inputs.lock.json", q.SourceLockSHA256}, 8<<20); err != nil {
+ return m, nil, err
+ }
+ fm := map[string]Family{}
+ qm := map[string]QualifiedFamily{}
+ for _, f := range q.Families {
+ if _, ok := qm[f.Name]; ok {
+ return m, nil, fmt.Errorf("duplicate qualification family")
+ }
+ qm[f.Name] = f
+ }
+ for _, f := range m.Families {
+ _, allowed := families[f.Name]
+ _, duplicate := fm[f.Name]
+ qualified, exists := qm[f.Name]
+ if !allowed || duplicate || f.Source != f.Name || !versionPattern.MatchString(f.Version) || f.Provenance.Path != "provenance/"+f.Name+"/provenance.json" || !exists || qualified.Status != "qualified" || qualified.ProvenanceSHA256 != f.Provenance.SHA256 {
+ return m, nil, fmt.Errorf("invalid family %q", f.Name)
+ }
+ pb, err := boundRead(root, f.Provenance, 32<<20)
+ if err != nil {
+ return m, nil, err
+ }
+ var provenance struct{ SourceLockSHA256, LockSHA256 string }
+ if err = decode(pb, &provenance, false); err != nil {
+ return m, nil, err
+ }
+ pin := provenance.SourceLockSHA256
+ if pin == "" {
+ pin = provenance.LockSHA256
+ }
+ if !digestPattern.MatchString(pin) || pin != qualified.NativeInput.LockSHA256 {
+ return m, nil, fmt.Errorf("source provenance lock mismatch: %s", f.Name)
+ }
+ fm[f.Name] = f
+ }
+ result := map[string]Payload{}
+ for _, p := range m.Packages {
+ f, exists := fm[p.Family]
+ allowed := false
+ for _, n := range families[p.Family] {
+ if n == p.Name {
+ allowed = true
+ }
+ }
+ _, duplicate := result[p.Name]
+ if !exists || !allowed || duplicate || p.Source != f.Source || p.Version != f.Version || p.Architecture != "amd64" || !strings.HasPrefix(p.Archive.Path, "debs/") || path.Base(p.Archive.Path) != strings.TrimPrefix(p.Archive.Path, "debs/") || !strings.HasSuffix(p.Archive.Path, ".deb") || p.Payload.Path != "payload/"+p.Name+".json" {
+ return m, nil, fmt.Errorf("invalid package catalog entry %q", p.Name)
+ }
+ if _, err = boundRead(root, p.Archive, 256<<20); err != nil {
+ return m, nil, err
+ }
+ pb, err := boundRead(root, p.Payload, 32<<20)
+ if err != nil {
+ return m, nil, err
+ }
+ var payload Payload
+ if err = decode(pb, &payload, true); err != nil {
+ return m, nil, err
+ }
+ if payload.SchemaVersion != 1 || payload.Package != p.Name || payload.Version != p.Version || payload.Source != p.Source || payload.Architecture != p.Architecture || len(payload.Files) == 0 {
+ return m, nil, fmt.Errorf("payload identity mismatch: %s", p.Name)
+ }
+ result[p.Name] = payload
+ }
+ return m, result, nil
+}
diff --git a/server/security/ubuntu-backports/bundle/qualify-native-packages.py b/server/security/ubuntu-backports/bundle/qualify-native-packages.py
new file mode 100644
index 0000000000..927a1ffd34
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/qualify-native-packages.py
@@ -0,0 +1,729 @@
+#!/usr/bin/env python3
+"""Qualify exact native package inputs for a build-only OCI bundle; never install.
+
+CLI inputs are the checked-in lock, current checkout and a fixed disposable
+staging directory. A pending native artifact blocks qualification/publication.
+"""
+import argparse
+import base64
+from collections import Counter
+import hashlib
+import importlib.util
+import io
+import json
+import os
+from pathlib import Path, PurePosixPath
+import re
+import shutil
+import stat
+import struct
+import subprocess
+import sys
+import tarfile
+import tempfile
+from urllib.parse import quote
+import urllib.error
+import urllib.request
+import zipfile
+
+HERE = Path(__file__).resolve().parent
+ROOT = HERE.parents[3]
+LOCK_FILE = HERE / 'source-inputs.lock.json'
+RUST_AUDIT_FILE = HERE / 'rust-coreutils-original18-source-closure.json'
+RUST_AUDIT_RECEIPT_SHA = '73189028622c0a0fc074067b4c5b8521094921531f95cf3e68073baaebd2fbbd'
+WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.0'
+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'],
+}
+BASE = 'server/security/ubuntu-backports/'
+
+
+def require(ok, message):
+ if not ok:
+ raise ValueError(message)
+
+
+def sha(data):
+ return hashlib.sha256(data).hexdigest()
+
+
+def file_sha(path):
+ return sha(path.read_bytes())
+
+
+def write_json(path, value):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n', encoding='utf-8')
+
+
+def safe_name(name):
+ require(isinstance(name, str) and name and '\\' not in name and not any(ord(c) < 32 for c in name),
+ 'Unsafe artifact path')
+ path = PurePosixPath(name)
+ require(path.parts and not path.is_absolute() and '..' not in path.parts and ':' not in name
+ and str(path) == name.rstrip('/'),
+ 'Artifact path escapes its root')
+ return path
+
+
+def read_zip(path, digest):
+ require(file_sha(path) == digest, 'Native ZIP differs from the reviewed artifact pin')
+ result, names, total = {}, set(), 0
+ with zipfile.ZipFile(path) as archive:
+ require(len(archive.infolist()) <= 10000, 'Too many native artifact entries')
+ for info in archive.infolist():
+ safe_name(info.orig_filename)
+ safe_name(info.filename)
+ key = info.filename.rstrip('/').casefold()
+ require(key not in names and not info.flag_bits & 1, 'Duplicate/encrypted ZIP entry')
+ names.add(key)
+ mode = info.external_attr >> 16
+ require(not stat.S_ISLNK(mode) and stat.S_IFMT(mode) in (0, stat.S_IFREG, stat.S_IFDIR),
+ 'ZIP special files are forbidden')
+ total += info.file_size
+ require(info.file_size <= 256 * 1024 * 1024 and total <= 1024 * 1024 * 1024,
+ 'Native ZIP exceeds bounded evidence size')
+ require('unqualified' not in info.filename.lower(), 'Unqualified diagnostics cannot be inputs')
+ if not info.is_dir():
+ result[info.filename] = archive.read(info)
+ return result
+
+
+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,
+ 'Unexpected bundle identity')
+ require(set(lock['nativeInputs']) == set(FAMILIES) - {'pcre2'}, 'Wrong native family set')
+ for family, item in lock['nativeInputs'].items():
+ require(item['runtimePackages'] == FAMILIES[family], 'Unexpected native runtime package set')
+ 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,
+ 'Pending input must not invent an artifact')
+ require(not ready, 'glibc 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']),
+ 'Native artifact is not pinned/qualified')
+ require(len(item['packages']) == len(FAMILIES[family])
+ and {p['name'] for p in item['packages']} == set(FAMILIES[family]), 'Wrong pinned deb set')
+ for package in item['packages']:
+ 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'
+ 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')
+ load_rust_source_audit(lock['nativeInputs']['rust-coreutils'])
+ return lock
+
+
+def load_rust_source_audit(item, provenance=None):
+ ref = item.get('sourceAudit', {})
+ require(ref.get('path') == RUST_AUDIT_FILE.name
+ and ref.get('originalReceiptSha256') == RUST_AUDIT_RECEIPT_SHA
+ and file_sha(RUST_AUDIT_FILE) == ref.get('sha256'), 'Rust original18 source audit is missing or changed')
+ public = json.loads(RUST_AUDIT_FILE.read_text(encoding='utf-8')); audit = public['audit']
+ require(public['schemaVersion'] == 1 and public['originalReceiptSha256'] == RUST_AUDIT_RECEIPT_SHA
+ and public['projection']['excludedFields'] == ['identity.originalZip']
+ and 'originalZip' not in audit['identity'] and audit['schemaVersion'] == 1
+ and audit['status'] == 'PASS_ORIGINAL18_SOURCE_CLOSURE', 'Unapproved Rust audit projection/status')
+ native = json.loads((HERE.parent / 'rust-coreutils' / 'rust-coreutils.lock.json').read_text(encoding='utf-8'))
+ identity, package = audit['identity'], item['packages'][0]
+ for field, expected in {'ciRun': item['runId'], 'ciJob': item['jobId'], 'artifactId': item['artifactId'],
+ 'producerCommit': item['sourceCommit'], 'originalZipSha256': item['zipSha256'],
+ 'lockSha256': item['lockSha256'], 'provenanceSha256': item['evidence']['provenance.json'],
+ 'packageSha256': package['sha256'], 'upstreamCommit': native['upstream']['baseCommit'],
+ 'ubuntuSourceVersion': native['ubuntuSourceVersion'], 'sourcePublication': native['sourcePublication']}.items():
+ require(identity[field] == expected, 'Rust source audit native identity mismatch: ' + field)
+ require(audit['scope']['componentPackage'] == package['file']
+ and audit['scope']['customVersion'] == package['version']
+ and [{k: f[k] for k in ('name', 'url', 'sha256', 'size')} for f in identity['sourceFiles']] == native['sourceFiles']
+ and all(f['verified'] is True for f in identity['sourceFiles'])
+ and identity['officialUpstreamArchive']['freshlyReadEqualsUbuntuOrig'] is True
+ and identity['officialUpstreamArchive']['sha256'] == native['upstream']['archiveSha256'],
+ 'Rust source audit original Ubuntu/upstream inputs disagree')
+ original = {r['id']: r for r in native['alreadyPresentFixes']}
+ rows = {r['id']: r for r in audit['results']}
+ new = {c for p in native['patches'] for c in p['fixedCves']}
+ require(len(audit['results']) == len(original) == 18 and set(rows) == set(original)
+ and len(item['fixedVulnerabilities']) == len(set(item['fixedVulnerabilities']))
+ and set(item['fixedVulnerabilities']) == set(original) | new, 'Rust fixed set lacks exact original18 audit coverage')
+ commits = {c['commit']: c for c in audit['officialCommitPatchEvidence']}
+ for identifier, row in rows.items():
+ require(row['status'] == 'fixed' and row['sourceFixPresent'] is True and row['effectiveRevertFound'] is False
+ and row['verdictScope'] == 'qualified-component-source-closure-only'
+ and row['lockMergeOrCompletionAnchor'] == original[identifier]['commit']
+ and row['additionalLockCommits'] == original[identifier].get('additionalCommits', [])
+ and row['sourceEvidence'] and row['officialCodeCommits']
+ and all(c in commits and re.fullmatch('[0-9a-f]{64}', commits[c]['officialPatchSha256'])
+ for c in row['officialCodeCommits'])
+ and row['existingNativeLog']['reused'] is True
+ and row['existingNativeLog']['completeCveSpecificDynamicCoverageClaimed'] is False,
+ 'Rust CVE lacks source/control/official-commit evidence: ' + identifier)
+ for evidence in row['sourceEvidence']:
+ safe_name(evidence['path'])
+ require(re.fullmatch('[0-9a-f]{64}', evidence['sha256'])
+ and type(evidence['lineStart']) is int and 0 < evidence['lineStart'] <= evidence['lineEnd']
+ and (evidence['path'] not in native['baselineSha256']
+ or evidence['sha256'] == native['baselineSha256'][evidence['path']]), 'Rust source evidence pin/line mismatch')
+ validation = audit['validation']; boundary = audit['sourceBuildBoundary']['customPatch']
+ require(validation['status'] == 'PASS' and validation['originalCves'] == validation['nativeSourcePinsMatched'] == 18
+ and all(validation[k] is True for k in ['rawPackageShaMatched', 'producerLockBytesMatch',
+ 'ubuntuOrigEqualsFreshOfficialArchive', 'originalUbuntuQuiltSourceControlsReviewed'])
+ and validation['newBuildTestScanExecuted'] is False
+ and audit['ubuntuQuilt']['matchedNativeBaselineFilePins'] == 18
+ and audit['ubuntuQuilt']['originalRulesAndPackagingPreserved'] is True
+ and boundary['sha256'] == native['patches'][0]['sha256']
+ and boundary['onlyTargetPaths'] == ['src/uu/id/src/id.rs', 'tests/by-util/test_id.rs']
+ and boundary['original18SourceUntouched'] is True, 'Rust bounded source/quilt validation is incomplete')
+ if provenance is not None:
+ reused, actual = audit['nativeEvidenceReused']['strictOwningSuite'], provenance['strictOwningSuite']
+ require(reused['observedTotals'] == actual['observedTotals']
+ and reused['strictLogSha256'] == actual['sha256']
+ and reused['lognameCoverage'] == actual['lognameCoverage']
+ and reused['originalUbuntuRulesSha256'] == actual['owningTestContract']['rulesSha256']
+ and audit['ubuntuQuilt']['seriesSha256'] == provenance['packagingPreserved']['debian/patches/series']
+ and all(p['sha256'] == provenance['packagingPreserved']['debian/patches/' + p['patch']]
+ for p in audit['ubuntuQuilt']['enabledPatches']), 'Rust audit/native limits or quilt receipt mismatch')
+ return public
+
+
+def check_source_files(root, expected):
+ for name, digest in expected.items():
+ safe_name(name)
+ require(re.fullmatch('[0-9a-f]{64}', digest), 'Invalid source SHA-256')
+ if file_sha(root / name) != digest:
+ # Windows checkouts may apply core.autocrlf. Accept only the exact
+ # repository clean-filter result, not arbitrary text normalization.
+ canonical = subprocess.check_output(['git', 'show', 'HEAD:' + name], cwd=root)
+ actual = subprocess.check_output(['git', 'hash-object', '--path=' + name, name], cwd=root).strip()
+ committed = subprocess.check_output(['git', 'rev-parse', 'HEAD:' + name], cwd=root).strip()
+ require(sha(canonical) == digest and actual == committed,
+ 'Current module/CI source closure changed: ' + name)
+
+
+def check_current_source(lock):
+ # Squash merging and deleting a candidate branch must not make its exact
+ # reviewed producer commit disappear from a fresh main checkout.
+ ensure_native_sources(lock)
+ for item in lock['nativeInputs'].values():
+ check_source_files(ROOT, item['sourceFiles'])
+ for name, digest in item['sourceFiles'].items():
+ original = command(['git', 'show', item['sourceCommit'] + ':' + name])
+ require(sha(original) == digest, 'Module source differs from the qualified native commit: ' + name)
+ for name, proof in item['ciClosure'].items():
+ 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')
+
+
+def ensure_native_sources(lock):
+ for commit in sorted({item['sourceCommit'] for item in lock['nativeInputs'].values()}):
+ 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:
+ subprocess.run(['git', 'fetch', '--no-tags', 'origin', commit], cwd=ROOT, check=True)
+ subprocess.run(probe, cwd=ROOT, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+
+def check_pcre2_source(lock):
+ source = ROOT.parent / 'pcre2-source'
+ require(command(['git', '-C', str(source), 'rev-parse', 'HEAD']).decode().strip() == lock['pcre2']['sourceCommit'],
+ 'Wrong production PCRE2 checkout')
+ check_source_files(source, lock['pcre2']['sourceFiles'])
+
+
+def gh_json(endpoint):
+ return json.loads(subprocess.check_output(['gh', 'api', 'repos/' + REPO + '/' + endpoint]))
+
+
+def verify_api(family, item, run, job, artifact):
+ require(run['id'] == item['runId'] and run['status'] == 'completed'
+ and run['head_sha'] == item['sourceCommit']
+ and run['conclusion'] == item['runConclusion']
+ and run['path'] == '.github/workflows/ubuntu-backport-native.yml', 'Native run API mismatch')
+ # jq's selected job passed in a matrix run whose other jobs failed. Preserve
+ # that run conclusion; never promote the whole run to success.
+ require(job['id'] == item['jobId'] and job['name'] == 'Native (' + family + ')'
+ and job['status'] == 'completed' and job['conclusion'] == 'success'
+ and job['head_sha'] == item['sourceCommit'] and job['run_id'] == item['runId'],
+ 'Selected native job did not succeed')
+ require(artifact['id'] == item['artifactId'] and not artifact['expired']
+ and artifact['digest'] == 'sha256:' + item['zipSha256']
+ and artifact['name'] == item['artifactName']
+ and artifact['workflow_run']['id'] == item['runId']
+ and artifact['workflow_run']['head_sha'] == item['sourceCommit'], 'Artifact API 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():
+ 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)
+ 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/' +
+ str(item['artifactId']) + '/zip'], stdout=stream, check=True)
+ require(file_sha(WORK / 'inputs' / (family + '.zip')) == item['zipSha256'], 'Downloaded ZIP pin mismatch')
+
+
+def registry_manifest(mode):
+ headers = {}
+ if mode != 'anonymous':
+ credentials = (os.environ['GITHUB_ACTOR'] + ':' + os.environ['REGISTRY_TOKEN']).encode()
+ headers['Authorization'] = 'Basic ' + base64.b64encode(credentials).decode()
+ scope = 'pull' if mode == 'anonymous' else 'pull,push'
+ request = urllib.request.Request('https://ghcr.io/token?service=ghcr.io&scope=repository:pasturestack/ubuntu-security-backports:' + scope,
+ headers=headers)
+ with urllib.request.urlopen(request, timeout=30) as response:
+ 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)
+ try:
+ with urllib.request.urlopen(request, timeout=30) as response:
+ data = response.read(4 * 1024 * 1024 + 1)
+ require(len(data) <= 4 * 1024 * 1024, 'Registry manifest exceeds size bound')
+ except urllib.error.HTTPError as error:
+ body = json.load(error); codes = {entry['code'] for entry in body.get('errors', [])}
+ 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')
+ 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')
+ (WORK / 'publisher' / (mode + '-manifest.json')).write_bytes(data)
+
+
+def module_helper(family):
+ script = 'build-jq-release.py' if family == 'jq' else 'build-' + family + '-backport.py'
+ file = ROOT / BASE / family / script
+ spec = importlib.util.spec_from_file_location('qualified_' + family.replace('-', '_'), file)
+ module = importlib.util.module_from_spec(spec)
+ sys.dont_write_bytecode = True
+ spec.loader.exec_module(module)
+ return module
+
+
+def check_owning_tests(family, files, provenance, directory):
+ directory.mkdir()
+ if family in ('jq', 'pcre2'):
+ expected = ['pcre2_jit_test', 'RunTest', 'RunGrepTest'] if family == 'pcre2' else [
+ 'mantest', 'jqtest', 'shtest', 'utf8test', 'base64test', 'uritest', 'optionaltest', 'onigtest', 'manonigtest']
+ tests = provenance['tests']
+ require([row['test'] for row in tests] == expected, 'Owning test set changed')
+ for row in tests:
+ data = files[row['test'] + '.trs']
+ require(sha(data) == row['sha256'] and row['status'] == 'PASS'
+ and re.findall(rb'^:test-result: (\S+)$', data, re.M) == [b'PASS']
+ and re.findall(rb'^:global-test-result: (\S+)$', data, re.M) == [b'PASS']
+ and row['test'] + '.log' in files, 'Native owning test did not pass')
+ if family == 'jq':
+ smoke = provenance['packageSmoke']
+ require(smoke['status'] == 'PASS' and smoke['validInputCases'] == 4
+ and smoke['soname'] == 'libjq.so.1' and smoke['systemOniguruma'] == 'libonig.so.5'
+ and smoke['publicAbiSymbols'] > 0, 'jq original ABI/system oniguruma smoke missing')
+ return {'status': 'qualified', 'owningTests': expected}
+ helper = module_helper(family)
+ if family == 'p11-kit':
+ suite = provenance['tests']['fullOwningSuite']
+ (directory / 'test-suite.log').write_bytes(files['test-suite.log'])
+ for row in suite['receipts']:
+ path = safe_name(row['path'])
+ for suffix in ('.trs', '.log'):
+ name = str(path.with_suffix(suffix))
+ target = directory / name
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_bytes(files[name.replace('/', '__')])
+ result = helper.full_suite(directory, suite['configuredTestLogs'])[0]
+ require(result['counts'] == suite['counts'] and result['uniqueCaseTotal'] == suite['uniqueCaseTotal'],
+ 'P11 actual owning evidence disagrees with provenance')
+ require(provenance['tests']['packagedCliAndPkcs11'] == 'PASS', 'P11 package smoke missing')
+ return {'status': 'qualified', 'counts': result['counts'], 'uniqueCaseTotal': result['uniqueCaseTotal'],
+ 'architectureCaveat': provenance['tests']['cve18938ArchitectureCaveat']}
+ if family == 'rust-coreutils':
+ log = directory / 'strict-tests.log'; log.write_bytes(files['strict-tests.log'])
+ suite = provenance['strictOwningSuite']
+ result = helper.strict_test_receipt(log, suite['owningTestContract'],
+ 'true' if suite['lognameCoverage']['ciMarkerForwarded'] else None)
+ require(result['sha256'] == suite['sha256'] and len(result['idCasesReportedOk']) == 19,
+ 'Rust owning-test evidence changed')
+ credentials = provenance['distinctCredentialBinaryTests']
+ cases = [(65534, 1, [2]), (1, 65534, [1, 2, 65534]), (65534, 65534, [2])]
+ require([(c['realGid'], c['effectiveGid'], c['supplementaryGids']) for c in credentials] == cases,
+ 'Packaged distinct-GID case set changed')
+ for case in credentials:
+ real, effective, groups = case['realGid'], case['effectiveGid'], case['supplementaryGids']
+ outputs = case['outputs']; expected = list(dict.fromkeys([real, effective] + sorted(groups)))
+ require(case['status'] == 'PASS' and all(list(map(int, outputs[flag].split())) == expected
+ for flag in ['-G', '-G -r']) and outputs['-g'] == str(effective)
+ and outputs['-g -r'] == str(real), 'Packaged distinct-GID numeric outputs failed')
+ match = re.search(r' groups=([^\s]+)', outputs[''])
+ actual = [int(x.split('(')[0]) for x in match[1].split(',')] if match else []
+ require(actual == list(dict.fromkeys([effective] + sorted(groups))), 'Packaged default GID ordering failed')
+ return {'status': 'qualified', 'idCases': 19, 'credentialCases': 3,
+ 'owningTestContract': suite['owningTestContract'], 'lognameCoverage': suite['lognameCoverage']}
+ if family == 'libxrender':
+ cases = json.loads(files['reply-boundary.json'])
+ require([row['test'] for row in cases] == ['patched', 'original', 'packaged'], 'XRender fixture set changed')
+ for row in cases:
+ helper.check_result(row['exitCode'], row['output'], row['test'] == 'original')
+ require(provenance['tests']['owningCheck'] == 'PASS'
+ and provenance['tests']['upstreamDefinedTestCases'] == 0, 'XRender owning contract changed')
+ return {'status': 'qualified', 'upstreamDefinedTestCases': 0, 'replyBoundaryCases': cases}
+ require(family == 'glibc', 'Unknown owning-test family')
+ before, approved = provenance['debianPackagingInputSha256'], provenance['approvedPackagingSha256']
+ generated = provenance['generatedControlMetadata']
+ # The original quilt series is extended by exactly the two approved patches.
+ additions = {'debian/patches/' + p['name']: p['sha256'] for p in provenance['patches']}
+ require(set(approved) == set(before) | set(additions)
+ and all(approved[name] == digest for name, digest in additions.items())
+ and sorted(name for name in before if before[name] != approved[name]) == ['debian/control', 'debian/patches/series']
+ and before['debian/control'] == helper.CONTROL_ORIGINAL_SHA256 == generated['originalSha256']
+ and approved['debian/control'] == helper.CONTROL_GENERATED_SHA256 == generated['generatedSha256']
+ and sha(files['generated-debian-control']) == helper.CONTROL_GENERATED_SHA256
+ and all(before[name] == digest for name, digest in generated['inputSha256'].items()),
+ 'glibc original-template control generation proof changed')
+ entries = helper.parse_test_summary(files['tests.sum'].decode('utf-8'))
+ counts = dict(Counter(state for state, _ in entries)); states = {name: state for state, name in entries}
+ require(not any(counts.get(s, 0) for s in ['FAIL', 'ERROR', 'UNRESOLVED'])
+ and all(states.get(name) == 'PASS' for name in helper.CONTROLS)
+ and counts == provenance['tests']['counts']
+ and sha(files['tests.sum']) == provenance['tests']['sha256']
+ and provenance['tests']['ubuntuGate'] == 'check_libc_passed', 'glibc native test contract failed')
+ for component in ['libio', 'stdio-common', 'iconv', 'wcsmbs', 'malloc', 'elf', 'nptl', 'nss']:
+ require(any(state == 'PASS' and name.startswith(component + '/') for state, name in entries),
+ 'glibc component suite did not execute: ' + component)
+ for name in helper.CONTROLS:
+ require(name.rsplit('/', 1)[-1] + '.out' in files
+ and b'PASS' in files[name.rsplit('/', 1)[-1] + '.test-result'], 'glibc control output is missing')
+ return {'status': 'qualified', 'counts': counts, 'controls': provenance['tests']['controls']}
+
+
+def validate_native(family, item, archive, api, root, test_directory):
+ verify_api(family, item, api['run'], api['job'], api['artifact'])
+ check_source_files(root, item['sourceFiles'])
+ contents = read_zip(archive, item['zipSha256'])
+ require(contents['source-sha.txt'].decode().strip() == item['sourceCommit'], 'Artifact checkout identity mismatch')
+ result = json.loads(contents['export/result.json'])
+ require(result['module'] == family and result['sourceSha'] == item['sourceCommit']
+ and result['status'] == 'passed'
+ and all(type(result[k]) is int and result[k] == 0 for k in ['exitCode', 'helperExitCode', 'logExitCode'])
+ and result['sourceLockSha256'] == item['lockSha256'], 'Native exit/lock receipt failed')
+ files = {name.removeprefix('export/package/'): data for name, data in contents.items()
+ if name.startswith('export/package/')}
+ for name, digest in item['evidence'].items():
+ require(name in files and sha(files[name]) == digest, 'Native evidence pin mismatch: ' + name)
+ require(sha(files[item['copyright']['file']]) == item['copyright']['sha256'], 'Original COPYRIGHT pin mismatch')
+ provenance = json.loads(files['provenance.json'])
+ require(provenance.get('sourceLockSha256', provenance.get('lockSha256')) == item['lockSha256']
+ and sha(files[family + '.lock.json']) == item['lockSha256'], 'Native source lock mismatch')
+ records = provenance.get('runtimePackages') or [{'package': family, 'file': provenance['package'],
+ 'sha256': provenance['packageSha256']}]
+ require({r['package'] for r in records} == set(item['runtimePackages']), 'Unexpected native runtime package catalog')
+ for package in item['packages']:
+ require(sha(files[package['file']]) == package['sha256']
+ and any(r['package'] == package['name'] and r['file'] == package['file']
+ and r['sha256'] == package['sha256'] for r in records), 'Deb differs from reviewed native bytes')
+ require({name for name in files if name.endswith('.deb')} == {p['file'] for p in item['packages']},
+ 'Extra native debs are forbidden')
+ tests = check_owning_tests(family, files, provenance, test_directory)
+ if family == 'rust-coreutils': load_rust_source_audit(item, provenance)
+ return files, provenance, tests
+
+
+def command(args):
+ return subprocess.check_output(args, cwd=ROOT, stderr=subprocess.PIPE)
+
+
+def package_payload(archive, expected, directory):
+ fields = {key: command(['dpkg-deb', '--field', str(archive), key]).decode().strip()
+ for key in ['Package', 'Version', 'Architecture', 'Source']}
+ require(fields['Package'] == expected['name'] and fields['Version'] == expected['version']
+ and fields['Architecture'] == 'amd64' and fields['Source'] in
+ ['', expected['source'], expected['source'] + ' (' + expected['version'] + ')'], 'Deb control identity mismatch')
+ require(fields['Source'] or expected['name'] == expected['source'], 'Deb Source omission is not permitted here')
+ data = command(['dpkg-deb', '--fsys-tarfile', str(archive)])
+ files, blobs, pending, elf, members, elf_cache = [], {}, [], [], {}, {}
+ with tarfile.open(fileobj=io.BytesIO(data)) as source:
+ require(sum(m.size for m in source.getmembers()) < 1024 * 1024 * 1024, 'Deb payload exceeds size bound')
+ for member in source.getmembers():
+ name = member.name.removeprefix('./').rstrip('/')
+ if name == '.' and member.isdir(): continue
+ if not name and member.isdir(): continue
+ path = str(safe_name(name))
+ require(path not in members, 'Duplicate deb payload path')
+ members[path] = member
+ if member.isdir(): continue
+ item = {'path': path, 'mode': stat.S_IMODE(member.mode), 'uid': member.uid, 'gid': member.gid}
+ if member.issym():
+ item.update(type='symlink', target=member.linkname)
+ elif member.isreg():
+ content = source.extractfile(member).read(); blobs[path] = content
+ item.update(type='file', sha256=sha(content))
+ elif member.islnk():
+ item.update(type='file'); pending.append((item, str(safe_name(member.linkname.removeprefix('./')))))
+ else:
+ raise ValueError('Special deb payload entries are forbidden')
+ files.append(item)
+ while pending:
+ next_pending = []
+ for item, target in pending:
+ if target in blobs:
+ blobs[item['path']] = blobs[target]; item['sha256'] = sha(blobs[target])
+ else: next_pending.append((item, target))
+ require(len(next_pending) < len(pending), 'Unresolved/cyclic deb hardlink')
+ pending = next_pending
+ for item in files:
+ require(not any(parent.as_posix() in members and not members[parent.as_posix()].isdir()
+ for parent in PurePosixPath(item['path']).parents), 'Deb entry has a non-directory parent')
+ if item['type'] != 'file' or not blobs[item['path']].startswith(b'\x7fELF'): continue
+ content = blobs[item['path']]
+ require(len(content) >= 64 and content[4:6] == b'\x02\x01' and struct.unpack_from('= 64 and binary[:6] == b'\x7fELF\x02\x01'
+ and struct.unpack_from('= 4, 'Consumer install/verify/console evidence is missing')
+ records.append({'provider': provider, 'status': 'PASS', 'inputManifestSha256': manifest_sha,
+ 'selectedPackages': selected, 'checks': checks, 'evidence': evidence})
+ qualification['consumerFixtures'] = records
+ qualification['consumerStatus'] = ('Both isolated provider fixtures passed actual installation and verification; '
+ 'real consumer image rootfs scanning and product acceptance remain required.')
+ write_json(output / 'qualification.json', qualification)
+ manifest = json.loads((output / 'manifest.json').read_text())
+ manifest['qualification']['sha256'] = file_sha(output / 'qualification.json')
+ write_json(output / 'manifest.json', manifest)
+ write_checksums(output)
+ print('BOTH_CONSUMER_INSTALL_FIXTURES_PASS')
+
+
+def verify_readback():
+ expected, actual = WORK / 'context' / 'out', WORK / 'readback' / 'out'
+ inventory = lambda root: {file.relative_to(root).as_posix(): file_sha(file)
+ for file in root.rglob('*') if file.is_file() and not file.is_symlink()}
+ require(expected.is_dir() and actual.is_dir() and inventory(expected) == inventory(actual),
+ 'Anonymous OCI readback differs from qualified producer bytes')
+ require(not any(p.is_symlink() for p in actual.rglob('*')), 'OCI data readback contains a symlink')
+ require(file_sha(WORK / 'readback' / 'bundle-installer') == file_sha(expected / 'install-existing-backports'),
+ 'OCI installer copies differ')
+ print('ANONYMOUS_OCI_READBACK_OK')
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ mode = parser.add_mutually_exclusive_group(required=True)
+ for option in ['check-lock', 'require-ready', 'print-work-root', 'fetch', 'check-pcre2-source', 'qualify',
+ 'verify-readback', 'check-new-tag', 'verify-published-manifest', 'verify-anonymous-manifest',
+ 'record-consumer-tests']:
+ mode.add_argument('--' + option, action='store_true')
+ args = parser.parse_args()
+ if args.print_work_root: print(WORK); return
+ if args.verify_readback: verify_readback(); return
+ if args.record_consumer_tests: record_consumer_tests(); return
+ if args.check_new_tag: registry_manifest('absent'); return
+ if args.verify_published_manifest: registry_manifest('published'); return
+ if args.verify_anonymous_manifest: registry_manifest('anonymous'); return
+ lock = load_lock(ready=args.require_ready or args.fetch or args.qualify)
+ if args.check_lock or args.require_ready:
+ check_current_source(lock); print('BUNDLE_SOURCE_LOCK_OK'); return
+ if args.fetch: fetch_native(lock); return
+ if args.check_pcre2_source: check_pcre2_source(lock); return
+ qualify(lock)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError, zipfile.BadZipFile) as error:
+ print('BUNDLE_QUALIFICATION_FAILED: ' + str(error), file=sys.stderr)
+ sys.exit(1)
diff --git a/server/security/ubuntu-backports/bundle/rust-coreutils-original18-source-closure.json b/server/security/ubuntu-backports/bundle/rust-coreutils-original18-source-closure.json
new file mode 100644
index 0000000000..3216014e10
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/rust-coreutils-original18-source-closure.json
@@ -0,0 +1,1874 @@
+{
+ "audit": {
+ "actionsNotPerformed": [
+ "\u672a\u4fee\u6539repo\uff0fsource tree\uff0flock",
+ "\u672a\u4fee\u6539canonical\uff0fSQLite\uff0fExcel",
+ "\u672a\u7de8\u8b6f\u6216\u57f7\u884c\u4efb\u4f55\u65b0\u6e2c\u8a66",
+ "\u672a\u8dd1\u6383\u63cf\u5668\u6216\u65b0\u589eagents",
+ "\u672a\u767c\u5e03\uff0f\u90e8\u7f72\uff0f\u64cd\u4f5cVM\u6216\u6b63\u5f0f\u7ad9"
+ ],
+ "asOfUtc": "2026-10-24 20:08:38 UTC",
+ "identity": {
+ "artifactId": 12571737310,
+ "ciJob": 120951205262,
+ "ciRun": 40120637646,
+ "dscSha256ManifestEntriesVerified": 3,
+ "firstPartyIdentity": "first-party rebuild; not issued by Canonical",
+ "lockSha256": "e52adb460ec7b01accc2f3f9cb3dccc8923e2619d5ced41aae1722494c81bd41",
+ "officialUpstreamArchive": {
+ "bytes": 2671039,
+ "freshlyReadEqualsUbuntuOrig": true,
+ "sha256": "f8e68cd0e3629378f047544ead272161a83211c43f4985a9f52944e5db8f1a44",
+ "url": "https://codeload.github.com/uutils/coreutils/tar.gz/refs/tags/0.10.0"
+ },
+ "originalZipSha256": "7f4b4780a492425bb9de239d67c6d594ccd3e8c0c85ac03eaa85035248ac5e2b",
+ "packageSha256": "f90cdece3ffa01acc9153e3741725febb696e02db8ccf6c0d0c4f98ff93ec609",
+ "producerCommit": "07e4a7afa827cb45514a72aba12f03b1d30d121d",
+ "producerTree": "9b96026ba0e17b2a12269705ab19ff5f95fe098d",
+ "provenanceSha256": "f06fd1b74848aeb0ef9d23f38c3e0700e167d935e7c7fcf7879c8e3561c23391",
+ "signatures": "SHA/source identity\u5df2\u6838\uff1b\u672a\u5ba3\u7a31PGP\uff0fGit commit/tag signature\u9a57\u8b49\u3002\u5b98\u65b90.10 annotated tag API\u6a19\u793aunsigned\u3002",
+ "sourceFiles": [
+ {
+ "name": "rust-coreutils_0.10.0-1ubuntu2~26.04.1.dsc",
+ "sha256": "c7eb62e7d2a07ec84c18bb2a6694331bb32bd0bde316261cfbe2f90acb2b6d28",
+ "size": 8746,
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0-1ubuntu2~26.04.1.dsc",
+ "verified": true
+ },
+ {
+ "name": "rust-coreutils_0.10.0.orig.tar.gz",
+ "sha256": "f8e68cd0e3629378f047544ead272161a83211c43f4985a9f52944e5db8f1a44",
+ "size": 2671039,
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0.orig.tar.gz",
+ "verified": true
+ },
+ {
+ "name": "rust-coreutils_0.10.0.orig-l10n.tar.gz",
+ "sha256": "3df90fabddfb28c7ae1d31237fc8abf70bb1f6c1937d818008943a11e31cfd5b",
+ "size": 780526,
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0.orig-l10n.tar.gz",
+ "verified": true
+ },
+ {
+ "name": "rust-coreutils_0.10.0-1ubuntu2~26.04.1.debian.tar.xz",
+ "sha256": "61eda389122a85e90e542bf6b12c840ab68ee3741958db539f24a3599f5d4bb2",
+ "size": 22541108,
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0-1ubuntu2~26.04.1.debian.tar.xz",
+ "verified": true
+ }
+ ],
+ "sourcePublication": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/18747163",
+ "ubuntuSourceVersion": "0.10.0-1ubuntu2~26.04.1",
+ "upstreamCommit": "28b6856d7b215bf844b4223589cb54ade84f5223",
+ "upstreamRepository": "https://github.com/uutils/coreutils",
+ "upstreamTag": "0.10.0",
+ "upstreamTagObject": "53e6f64b1519ebd0b18dfaf07e7006f233bbcc46",
+ "upstreamTree": "e9b7a01352471d10a542b30580fa2f98dda761ac"
+ },
+ "limitations": [
+ "\u7d50\u8ad6\u50c5\u9069\u7528package SHA f90cdece\u2026\u53ca\u6b64\u7cbe\u6e96source chain\uff1b\u672a\u5ba3\u7a31\u73fe\u5f7917\u500bconsumer\u6216\u516820\u9805\u7522\u54c1\u5df2\u4fee\u3002",
+ "fixed\u662fqualified-component source closure\uff0c\u4e0d\u662fCanonical\u5b98\u65b9security\u66f4\u65b0\u72c0\u614b\uff0c\u4e5f\u4e0d\u662fcanonical/VEX disposition mutation\u3002",
+ "\u7121effective revert\u662f\u5c0d\u76ee\u6a19orig+enabled quilt\u53ca\u76ee\u524d\u5b89\u5168\u63a7\u5236\u7684\u5224\u65b7\uff0c\u6c92\u6709\u5ba3\u7a31\u5b8c\u6574Git\u6b77\u53f2\u5f9e\u672a\u51fa\u73fe\u4efb\u4f55revert\u63d0\u4ea4\u3002",
+ "\u539fstrict suite\u670984 upstream ignored\u820717\u65e2\u6709filtered\uff1b\u4e0d\u662fall-functional coverage\uff0clogname\u771f\u5be6login fixture\u4e5f\u672a\u8b49\u660e\u3002",
+ "dd\u8207split\u7684source unit regression\u540d\u7a31\u672a\u5728\u539fstrict log\u89c0\u5bdf\u5230\uff1bsource\u63a7\u5236\u53ef\u8b49\uff0c\u8a72unit\u7684\u65b0\u589e\u57f7\u884c\u672a\u7372\u6388\u6b0a\u4e14\u672a\u9032\u884c\u3002",
+ "\u5169\u500bid\u65b0CVE35370/35371\u6cbf\u7528\u5df2\u7d66\u5b9anative\u5408\u683c\u8b49\u64da\uff0c\u672a\u65bc\u672c\u8f2a\u91cd\u65b0\u5224\u5b9a\u6216\u91cd\u8dd1\u3002"
+ ],
+ "method": [
+ "\u539fZIP\u3001export lock\u3001provenance\u3001\u5be6\u969bdeb bytes\u9010\u4e00SHA\u7d81\u5b9a\uff1bgit show CI exact07e producer\u7684lock bytes\u5b8c\u5168\u76f8\u540c\u3002",
+ "\u56db\u500bUbuntu\u539fsource\u6a94\u9010\u4e00size/SHA\u6838\u5c0d\uff1b\u5b98\u65b90.10 archive\u91cd\u65b0\u8b80\u53d6\u8207Ubuntu orig\u5b8c\u5168\u540cbytes\uff0ctag\u7269\u4ef6\u2192commit\u2192tree\u5df2\u6838\u3002",
+ "16\u500block\u5b98\u65b9completion/fix anchors\u4ee5\u5b98\u65b9compare API\u6838\u5be6\u70ba0.10 commit\u7956\u5148\uff1b\u4e0d\u628a\u7956\u5148\u6216\u7248\u672c\u95dc\u4fc2\u55ae\u7368\u7576\u7121revert\u8b49\u64da\u3002",
+ "\u539fUbuntu quilt\u5b8c\u6574series\u53ca14\u500benabled patch target\u5df2\u6838\uff1b\u91dd\u5c0d18\u9805\u76f8\u95dcsource\uff0c\u5728\u8a18\u61b6\u9ad4\u91cd\u5efa\u5b8c\u6574exact-context hunks\uff0c18\u500bnative baseline file pins\u5168\u76f8\u7b26\uff0c\u4e0d\u5bebsource\u3002",
+ "\u4f9dCVE\u539f\u5b89\u5168\u908a\u754c\u8ffd\u67e5\u76ee\u524d\u5be6\u969bcontrol\uff0c\u542btail/watch\u3001cp Linux platform\u3001safe_copy\u3001mv fd-xattr\u3001env/split_iterator\u7b49shared paths\uff1b\u4e0d\u53ea\u6aa2\u67e5lock\u7684sourcePath\u3002",
+ "\u5c0d\u539ffix\u5df2\u88ab\u5f8c\u7e8c\u91cd\u69cb\u53d6\u4ee3\u8005\uff0c\u6838\u5c0d\u7b49\u50f9\u76ee\u524d\u63a7\u5236\uff1b\u5c0dcompletion commit\u53ea\u6539\u6e2c\u8a66\u8005\uff0c\u88dc\u5176\u5df2merged\u5b98\u65b9PR\u7684\u5be6\u969bcode commit\u3002",
+ "\u50c5\u89e3\u6790\u539f\u6210\u529fstrict-tests.log\uff0c\u4fdd\u7559ignored\uff0ffiltered\u8207\u672a\u89c0\u5bdf\u5230\u7684\u5c08\u5c6cunit test\u7bc4\u570d\uff1b\u672a\u57f7\u884c\u65b0\u7684build/test/scan\u3002"
+ ],
+ "nativeEvidenceReused": {
+ "alternativesChanged": false,
+ "builtAt": "2026-10-24T18:49:35.830033+00:00",
+ "newIdCredentialCases": 3,
+ "newlyBackportedCves": [
+ "CVE-2026-35370",
+ "CVE-2026-35371"
+ ],
+ "runtimeInstalled": false,
+ "rustc": "rustc 1.93.1 (01f6ddf75 2026-02-11) (built from a source tarball)",
+ "status": "native_build_and_required_tests_passed",
+ "strictOwningSuite": {
+ "lognameCoverage": {
+ "ciMarkerForwarded": true,
+ "formalOutcome": "reported ok",
+ "genuineLoginFixtureValidated": false,
+ "limitation": "The upstream CI guard may return on no login name before login assertions; these outcomes do not establish successful login-name retrieval.",
+ "tests": [
+ "test_logname::test_normal",
+ "test_logname::test_output_format"
+ ]
+ },
+ "newExclusionPatterns": [],
+ "observedTotals": {
+ "failed": 0,
+ "filteredOut": 17,
+ "measured": 0,
+ "passed": 5438,
+ "upstreamIgnored": 84
+ },
+ "originalUbuntuRulesSha256": "06c5fd183a4a1146748810e3cce859ba849643eeee244cc80cbb3315aeedb5e7",
+ "scope": "Pinned Ubuntu owning-test contract; not all-functional coverage",
+ "status": "PASS",
+ "strictLogSha256": "b4eba1bcfa7299a6fb2cbf76e19424cb1f19555779b71f14823a29b8bc43a3a7"
+ }
+ },
+ "officialCommitPatchEvidence": [
+ {
+ "apiResponseSha256": "9aca4e70e563c36e8172a55d501395ccbd622835a9c681d2810397453febf8cc",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "commit": "00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "officialPatchBytes": 2603,
+ "officialPatchSha256": "1ba9044bb81463f5686c2422a933649a185b7a66124cee46d4e04cd9003bf2a3",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/00f77cc7081f8e6837271d21b4fe8ca6d2f047c3.patch",
+ "paths": [
+ "src/uu/mkfifo/src/mkfifo.rs",
+ "tests/by-util/test_mkfifo.rs"
+ ],
+ "tree": "818e11303f42834e778e72f99fb32f1c60beae82"
+ },
+ {
+ "apiResponseSha256": "212cb06fca663ea30ce4baaf2945f5ae5079769c217ca82d05fe68f64649b1ed",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "commit": "0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "officialPatchBytes": 4353,
+ "officialPatchSha256": "0accf0902cc11a9cadccc2b27d96a4600b8e9fe16ac01683dfbdcab71519dc8a",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/0f538b942a5cdaba6e0960f6952429a1baaedf93.patch",
+ "paths": [
+ "src/uu/dd/src/dd.rs"
+ ],
+ "tree": "ef42d1c3e6d1596996bd48c68f06227b65a066b6"
+ },
+ {
+ "apiResponseSha256": "903733a437c3e6626accaa06dea4a2dfb7b1c800c8da0de5f7e57e48e6a724ff",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "commit": "2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "officialPatchBytes": 6436,
+ "officialPatchSha256": "dad593df6f1585d8286f5818733e2d2571d8a3df37eb213256fbabf48a823cd4",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/2c2f1b0c1f11f120fc8cbf8d518194f6532c0607.patch",
+ "paths": [
+ "src/uu/tail/locales/en-US.ftl",
+ "src/uu/tail/locales/fr-FR.ftl",
+ "src/uu/tail/src/follow/watch.rs",
+ "tests/by-util/test_tail.rs"
+ ],
+ "tree": "d1057779ddfc91bc09cd1483e0885a7984faf556"
+ },
+ {
+ "apiResponseSha256": "ee78d245fb901681d1255aa6cb9f32ca2e6587310e4eeb18a678a79a6854218d",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "commit": "48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "officialPatchBytes": 5122,
+ "officialPatchSha256": "6d8e21cf5b6c625ed62c844b058073a1d42102ee59d5567d415bdbc0e32c2961",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/48d030dfb2ca7659ee2735f09cda706c8245a3fc.patch",
+ "paths": [
+ "src/uu/ln/locales/en-US.ftl",
+ "src/uu/ln/locales/fr-FR.ftl",
+ "src/uu/ln/src/ln.rs",
+ "tests/by-util/test_ln.rs"
+ ],
+ "tree": "51de098847f7a7fde667dd1e4280c7eff29e9907"
+ },
+ {
+ "apiResponseSha256": "fc1b71fa201613ec608999d5e5a5a9bed6b8cafbdef400195ddb5566578b3d91",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/500dd65c539bff848c712bf7a8a111109a32eae9",
+ "commit": "500dd65c539bff848c712bf7a8a111109a32eae9",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/500dd65c539bff848c712bf7a8a111109a32eae9",
+ "officialPatchBytes": 13103,
+ "officialPatchSha256": "606ead540caf4391537ca39853f4c9564234770927f6bd91826eee415cc85a14",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/500dd65c539bff848c712bf7a8a111109a32eae9.patch",
+ "paths": [
+ "src/uu/mv/Cargo.toml",
+ "src/uu/mv/src/mv.rs",
+ "src/uucore/Cargo.toml",
+ "src/uucore/src/lib/features/fsxattr.rs",
+ "util/check-safe-traversal.sh"
+ ],
+ "tree": "02a401b66312490bfdf3d48f90f9943bb3a0ba28"
+ },
+ {
+ "apiResponseSha256": "1f200f3d20597bd861e3a8b592c5674abba1e1465186f037195f389b0539fdaa",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/55298ee943231c07e944bd6e1a9cf9e01236d324",
+ "commit": "55298ee943231c07e944bd6e1a9cf9e01236d324",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/55298ee943231c07e944bd6e1a9cf9e01236d324",
+ "officialPatchBytes": 6072,
+ "officialPatchSha256": "eaf3a2c8482ddd371748981034f7f3fccc36287a04b2b800d088591a94157aeb",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/55298ee943231c07e944bd6e1a9cf9e01236d324.patch",
+ "paths": [
+ "src/uu/env/src/split_iterator.rs",
+ "tests/by-util/test_env.rs"
+ ],
+ "tree": "d9994aa9dc07e97b3a0fe2d85546b07389a0da41"
+ },
+ {
+ "apiResponseSha256": "dddc4e509185b70a8b8e007d0e6d1f7d567791bca2f731e1379cad715323624d",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "commit": "58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "officialPatchBytes": 16038,
+ "officialPatchSha256": "22f595b78d7ce795f6a205f8721e00afeb4c43d5c7ad972875f96625de66a986",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/58040feb5c66fce588a7ffac299b3a23bd55cd81.patch",
+ "paths": [
+ ".github/workflows/CICD.yml",
+ "src/uu/split/src/platform/mod.rs",
+ "src/uu/split/src/platform/unix.rs",
+ "src/uu/split/src/platform/windows.rs",
+ "src/uu/split/src/split.rs",
+ "util/check-safe-traversal.sh"
+ ],
+ "tree": "7ea333e0b5a0502cad5aa3aa3378264310fe076b"
+ },
+ {
+ "apiResponseSha256": "e8f9ab30a8628b7034e26f82bde9b7124c03d4699c9003d025edf601fbba31fe",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "commit": "6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "officialPatchBytes": 11906,
+ "officialPatchSha256": "48bb8ee8838c75e61e9d60c90431a2364158095d421a440fa5f25c784bd830c5",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/6b16cc9688fe998f5dc2786f53f22ceea709e318.patch",
+ "paths": [
+ "src/uu/rm/src/rm.rs",
+ "tests/by-util/test_rm.rs"
+ ],
+ "tree": "d221c2e3ef578e6a2001f1e8ff3b6645122fe644"
+ },
+ {
+ "apiResponseSha256": "fc004d1ec90e653583711849b019d7f9cfa1233a9ec70a4002fbdb8e981e9486",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/6e28633263ea84aad9ef7a30750232b73fb9133f",
+ "commit": "6e28633263ea84aad9ef7a30750232b73fb9133f",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/6e28633263ea84aad9ef7a30750232b73fb9133f",
+ "officialPatchBytes": 3154,
+ "officialPatchSha256": "14b4ffee0cc8a6f4830ef2c648860a193a4673ceab0c1913c0dc78af12b2e998",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/6e28633263ea84aad9ef7a30750232b73fb9133f.patch",
+ "paths": [
+ "src/uu/sort/src/sort.rs",
+ "tests/by-util/test_sort.rs"
+ ],
+ "tree": "4c63f8f9f660fc383128d83a758120ffea164821"
+ },
+ {
+ "apiResponseSha256": "f46792547c1a00ab2de0804092a2907e233b98edb8963789ab0ef6d576337741",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/7c8563d88ffa8f0dba5745587baad0d0c58de62d",
+ "commit": "7c8563d88ffa8f0dba5745587baad0d0c58de62d",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/7c8563d88ffa8f0dba5745587baad0d0c58de62d",
+ "officialPatchBytes": 10828,
+ "officialPatchSha256": "bbb3dabe9a42fe2bcd7801840ae4674541a2fa8f9616257ba368541ff032b3b7",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/7c8563d88ffa8f0dba5745587baad0d0c58de62d.patch",
+ "paths": [
+ "src/uu/cp/Cargo.toml",
+ "src/uu/cp/src/platform/linux.rs",
+ "src/uu/cp/src/platform/other_unix.rs",
+ "tests/by-util/test_cp.rs",
+ "util/check-safe-traversal.sh"
+ ],
+ "tree": "9aa79b2c831f3480a708db475d206d9c84995784"
+ },
+ {
+ "apiResponseSha256": "8c388af1c83a16507c1bbcdb16537f28e503e44c64cb361276c4cbe04925e0a8",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/874efa7cc3361cb5af2a97db869147f910bcab44",
+ "commit": "874efa7cc3361cb5af2a97db869147f910bcab44",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/874efa7cc3361cb5af2a97db869147f910bcab44",
+ "officialPatchBytes": 4587,
+ "officialPatchSha256": "8ea4a6888fff67f800a9ae733601122011f716157bbefb9fe03c9c445eb656de",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/874efa7cc3361cb5af2a97db869147f910bcab44.patch",
+ "paths": [
+ "src/uu/mv/Cargo.toml",
+ "src/uu/mv/src/mv.rs"
+ ],
+ "tree": "91db949af1f279fdc35019e9a1bd1f9d50991bd3"
+ },
+ {
+ "apiResponseSha256": "2857a4ae71868269e1d9510fb3c9237ca83c6aa2559682f01a852ae3a9fe4b56",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "commit": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "officialPatchBytes": 6300,
+ "officialPatchSha256": "9094bf44214a41e9bcfa6ddcc75aefcfadcf41d3b858993b6179db2c7d0c4181",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/aa30c103004463ffe7b8a98ca0ce519d56bbbd41.patch",
+ "paths": [
+ "src/uu/mv/src/mv.rs",
+ "src/uucore/src/lib/features/fsxattr.rs"
+ ],
+ "tree": "12f7037791792052b2ffc3030d4cad46db7983c7"
+ },
+ {
+ "apiResponseSha256": "79d1cc4caf442ae86a2a02ce9339a936b5aa1fd55b3e406a5f57edddc2489636",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/acadaa0e51ab82309ec81e6021d630b875cd3e10",
+ "commit": "acadaa0e51ab82309ec81e6021d630b875cd3e10",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/acadaa0e51ab82309ec81e6021d630b875cd3e10",
+ "officialPatchBytes": 34413,
+ "officialPatchSha256": "eec9d78c60d868dfdd1c4e64bd0811ab0636eb5a18d69686f52810e1ad9fcf3f",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/acadaa0e51ab82309ec81e6021d630b875cd3e10.patch",
+ "paths": [
+ "src/uu/cp/src/cp.rs",
+ "src/uu/cp/src/platform/linux.rs",
+ "src/uu/cp/src/platform/macos.rs",
+ "src/uu/cp/src/platform/other_unix.rs",
+ "tests/by-util/test_cp.rs",
+ "util/check-safe-traversal.sh"
+ ],
+ "tree": "8f26179b4f7a8d1064bb3b177f9da384d978b073"
+ },
+ {
+ "apiResponseSha256": "4e576a1fe4632cca7650792464b7241c283894ff15d18b99fd5719cbd954c93d",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "commit": "acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "officialPatchBytes": 1986,
+ "officialPatchSha256": "d383589178f9c5a9048204a1af3810f8bd55ff046003aa6968ab2f26c35b3ccf",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/acd51ef18d90e5c26f694df721f896fa1c4a9edc.patch",
+ "paths": [
+ "src/uu/nohup/src/nohup.rs"
+ ],
+ "tree": "d7d0432fd02e92030dc28309457af847678cda1d"
+ },
+ {
+ "apiResponseSha256": "8b90ed0577f665957d5af9db657d7a65d39cf8070ba14c0b8fb19ace67e54b97",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "commit": "b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "officialPatchBytes": 1387,
+ "officialPatchSha256": "c5d37a9cf4021220bb9b4b9e8740f08893bcd42e115cde98c792805a4a6b0b11",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/b9d8ff64c224eb425702584b66ec0ac64a99592a.patch",
+ "paths": [
+ "util/gnu-patches/tests_env_env-S.pl.patch"
+ ],
+ "tree": "4fc02226478b9ae2a054bcc2671c2e07fdb0b304"
+ },
+ {
+ "apiResponseSha256": "bd91796b5e652bd4769c3f802bd9fa2e25648e6d5b038d6efdeee230f81a24cf",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "commit": "bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "officialPatchBytes": 3772,
+ "officialPatchSha256": "81fb9e76addd04941b02081facb696ee5606b2bf2efa74790addb4e34f258cc1",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/bb8f7095fd63206ff54a4fb4256375e9e1ad88c9.patch",
+ "paths": [
+ ".vscode/cspell.dictionaries/jargon.wordlist.txt",
+ "src/uu/sort/src/sort.rs",
+ "tests/by-util/test_sort.rs"
+ ],
+ "tree": "144815709dc138761d669d05f634ee3769aef249"
+ },
+ {
+ "apiResponseSha256": "a051a49b53e56a0e2ca585a9ac4dba07ea7d72c2cb48699944ca9918b24b17ca",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "officialPatchBytes": 5171,
+ "officialPatchSha256": "109abb9cfe09dbc5adc3846bd8aa659f218e11c6b0e6243ad0b5d76c29fa43eb",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/bd170a5d4b19ed985417d322dffb8cade2cbbad5.patch",
+ "paths": [
+ "src/uu/cp/src/cp.rs",
+ "tests/by-util/test_cp.rs"
+ ],
+ "tree": "c6657a107aaecca493fee4ffce97a2cac0b35bc4"
+ },
+ {
+ "apiResponseSha256": "0f267705f6fd6bf597535a2bfe4f41e508b2701cd2ee77ab56c4309c810cc9c3",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/c004672c5eb2415b03f11f766583c4b967ba024f",
+ "commit": "c004672c5eb2415b03f11f766583c4b967ba024f",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/c004672c5eb2415b03f11f766583c4b967ba024f",
+ "officialPatchBytes": 10481,
+ "officialPatchSha256": "7868720880cef10a4b5cab137f50569cb3fb033b3c43c189a8428d8263437064",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/c004672c5eb2415b03f11f766583c4b967ba024f.patch",
+ "paths": [
+ "Cargo.lock",
+ "src/uu/mkfifo/Cargo.toml",
+ "src/uu/mkfifo/src/mkfifo.rs",
+ "util/check-toctou.sh"
+ ],
+ "tree": "d2790ab701598a77ffb0946a6ee55635f2bb644b"
+ },
+ {
+ "apiResponseSha256": "f86fd88a0b34d37924bb9af619a3a117581a8feafb2f8040c719816bde22b1f3",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "commit": "d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "officialPatchBytes": 7155,
+ "officialPatchSha256": "388b041099c7e9e9d32b912c1491c25162bf383403304bb9de5279543a08a64b",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/d296f8d39f3a71ef7c5bd3b949e65aa978f1436d.patch",
+ "paths": [
+ "src/uu/touch/src/touch.rs",
+ "tests/by-util/test_touch.rs",
+ "util/check-safe-traversal.sh"
+ ],
+ "tree": "99951d7cda510d447fbca502f9f8b93a12277617"
+ },
+ {
+ "apiResponseSha256": "741caa1a24d7ea486517c2ead36ee393dd41556820f25b74096198760a6e21ac",
+ "apiUrl": "https://api.github.com/repos/uutils/coreutils/commits/f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "commit": "f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "commitUrl": "https://github.com/uutils/coreutils/commit/f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "officialPatchBytes": 2585,
+ "officialPatchSha256": "8f9c740836c2ae97109f15ce7d979f4be7f1b62b42ce7bb191af2d57bc75eb7f",
+ "officialPatchUrl": "https://github.com/uutils/coreutils/commit/f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f.patch",
+ "paths": [
+ "src/uu/chroot/src/chroot.rs"
+ ],
+ "tree": "8a808807b339478b65e49023ffd05115c2ab3d55"
+ }
+ ],
+ "results": [
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 2082,
+ "behind_by": 0,
+ "commit": "00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "mergeBase": "00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "status": "ahead",
+ "total_commits": 2082,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/00f77cc7081f8e6837271d21b4fe8ca6d2f047c3...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_mkfifo::test_mkfifo_permission_unchanged_when_failed ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35341",
+ "lockMergeOrCompletionAnchor": "00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "officialAdvisory": {
+ "sha256": "779c435f1acb92ea0141ad218fa03ae8074ac9045b4041302a717c17a616b88f",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35341.json"
+ },
+ "officialCodeCommits": [
+ "00f77cc7081f8e6837271d21b4fe8ca6d2f047c3"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/10376",
+ "securityBoundary": {
+ "control": "\u76ee\u524d\u4ee5create_fifo\u539f\u5b50\u5efa\u7acb\u4e26\u5957\u6307\u5b9amode\uff0c\u5931\u6557\u53ea\u5831\u932f\uff1b\u5df2\u6c92\u6709\u5efa\u7acb\u5931\u6557\u5f8c\u4ecdchmod\u65e2\u6709\u6a94\u6848\u7684\u8def\u5f91\u3002\u539f00f77\u7684continue\u7531\u5f8c\u7e8cc004672c\u5b89\u5168\u5be6\u4f5c\u53d6\u4ee3\uff0c\u4e0d\u80fd\u4ee5continue\u6587\u5b57\u6d88\u5931\u5224\u5b9arevert\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u65e2\u6709\u6a94\u6848\u6b0a\u9650\u8b8a\u66f4",
+ "source": "\u5df2\u5b58\u5728\u7684FIFO\u76ee\u6a19\u6a94\u6848"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 78,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 72,
+ "path": "src/uu/mkfifo/src/mkfifo.rs",
+ "sha256": "6c7c49a911cff80e6ea0499044971bd232c37b298a5a0c83551b41d5a9f46dbe",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mkfifo/src/mkfifo.rs"
+ },
+ {
+ "lineEnd": 81,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 75,
+ "path": "src/uu/mkfifo/src/mkfifo.rs",
+ "sha256": "6c7c49a911cff80e6ea0499044971bd232c37b298a5a0c83551b41d5a9f46dbe",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mkfifo/src/mkfifo.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": "c004672c5eb2415b03f11f766583c4b967ba024f",
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 590,
+ "behind_by": 0,
+ "commit": "0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "mergeBase": "0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "status": "ahead",
+ "total_commits": 590,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/0f538b942a5cdaba6e0960f6952429a1baaedf93...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [],
+ "proofGap": "\u8a72source unit regression\u5b58\u5728\uff0c\u4f46\u672a\u5728\u65e2\u6709strict log\u89c0\u5bdf\u5230\u6b64test\u540d\uff1b\u672c\u8f2a\u4e0d\u91cd\u8dd1\uff0csource closure\u8207\u4e00\u822cnative PASS\u5206\u958b\u3002",
+ "reused": true
+ },
+ "id": "CVE-2026-35344",
+ "lockMergeOrCompletionAnchor": "0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "officialAdvisory": {
+ "sha256": "10fd01dbb0cbbd2c1b1a6e9c8e709441be6c89f3ff5727c26561cf1651626d13",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35344.json"
+ },
+ "officialCodeCommits": [
+ "0f538b942a5cdaba6e0960f6952429a1baaedf93"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12667",
+ "securityBoundary": {
+ "control": "Dest::truncate\u5c0d\u4e00\u822c\u6a94\u7684set_len\u5931\u6557\u56de\u50b3Err\uff0cBlockWriter\u4fdd\u7559Result\u4e14finalize\u4ee5?\u50b3\u64ad\uff1b\u7279\u6b8a\u8f38\u51fa\u4ecd\u4fdd\u7559GNU\u76f8\u5bb9\u8655\u7406\u3002Ubuntu dd-ensure-full-writes\u672a\u64a4\u56de\u6b64\u63a7\u5236\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u5047\u6210\u529f\u53ca\u820a\u8cc7\u6599\u6b98\u7559",
+ "source": "\u4e00\u822c\u8f38\u51fa\u6a94\u6848\u7684truncate\u5931\u6557"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 672,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 667,
+ "path": "src/uu/dd/src/dd.rs",
+ "sha256": "11052025db5fd3e1c5cac0b026c67b1d45c7cf601cff01329eba0f2408ad54b9",
+ "ubuntuQuiltTouches": [
+ {
+ "name": "dd-ensure-full-writes.patch",
+ "sha256": "dd2c5a3cc8dfb89dea54e68db55903859f09ab6f0bb6ff3e7cbf7c7ecc7f80b1"
+ }
+ ],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/dd/src/dd.rs"
+ },
+ {
+ "lineEnd": 1285,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1279,
+ "path": "src/uu/dd/src/dd.rs",
+ "sha256": "11052025db5fd3e1c5cac0b026c67b1d45c7cf601cff01329eba0f2408ad54b9",
+ "ubuntuQuiltTouches": [
+ {
+ "name": "dd-ensure-full-writes.patch",
+ "sha256": "dd2c5a3cc8dfb89dea54e68db55903859f09ab6f0bb6ff3e7cbf7c7ecc7f80b1"
+ }
+ ],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/dd/src/dd.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 394,
+ "behind_by": 0,
+ "commit": "2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "mergeBase": "2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "status": "ahead",
+ "total_commits": 394,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/2c2f1b0c1f11f120fc8cbf8d518194f6532c0607...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_tail::test_follow_name_replaced_by_symlink_is_untailable ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35345",
+ "lockMergeOrCompletionAnchor": "2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "officialAdvisory": {
+ "sha256": "9b50fe889d672a5204d90d20f5329539c1687311420b52ef8b8f0e178699ec37",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35345.json"
+ },
+ "officialCodeCommits": [
+ "2c2f1b0c1f11f120fc8cbf8d518194f6532c0607"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12661",
+ "securityBoundary": {
+ "control": "\u4ee5symlink_metadata\u8b58\u5225\u66ff\u63db\uff0c\u6a19\u70bauntailable\u4e26reset_reader\uff0c\u4e0d\u7e7c\u7e8c\u8b80\u53d6symlink\u76ee\u6a19\u3002\u5be6\u969b\u63a7\u5236\u4f4d\u65bcfollow/watch.rs\u800c\u975elock\u6240\u5217tail.rs\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u654f\u611f\u6a94\u6848\u5167\u5bb9\u8f38\u51fa",
+ "source": "--follow=name\u76e3\u770b\u6a94\u6848\u88ab\u63db\u6210symlink"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 328,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 322,
+ "path": "src/uu/tail/src/follow/watch.rs",
+ "sha256": "9c6a2bdbe2bd14bcc11efe3b56bdd606eb6bfa1074d6c9f2c577403f8c76b79f",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/tail/src/follow/watch.rs"
+ },
+ {
+ "lineEnd": 367,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 361,
+ "path": "src/uu/tail/src/follow/watch.rs",
+ "sha256": "9c6a2bdbe2bd14bcc11efe3b56bdd606eb6bfa1074d6c9f2c577403f8c76b79f",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/tail/src/follow/watch.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [
+ "6e28633263ea84aad9ef7a30750232b73fb9133f"
+ ],
+ "ancestry": [
+ {
+ "ahead_by": 250,
+ "behind_by": 0,
+ "commit": "6e28633263ea84aad9ef7a30750232b73fb9133f",
+ "mergeBase": "6e28633263ea84aad9ef7a30750232b73fb9133f",
+ "status": "ahead",
+ "total_commits": 250,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/6e28633263ea84aad9ef7a30750232b73fb9133f...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ },
+ {
+ "ahead_by": 425,
+ "behind_by": 0,
+ "commit": "bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "mergeBase": "bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "status": "ahead",
+ "total_commits": 425,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/bb8f7095fd63206ff54a4fb4256375e9e1ad88c9...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_sort::test_files0_from_1 ... ok",
+ "test test_sort::test_files0_from_1a ... ok",
+ "test test_sort::test_files0_from_2 ... ok",
+ "test test_sort::test_files0_from_2a ... ok",
+ "test test_sort::test_files0_from_empty ... ok",
+ "test test_sort::test_files0_from_empty_non_regular ... ok",
+ "test test_sort::test_files0_from_extra_arg ... ok",
+ "test test_sort::test_files0_from_minus_in_stdin ... ok",
+ "test test_sort::test_files0_from_missing ... ok",
+ "test test_sort::test_files0_from_non_utf8 ... ok",
+ "test test_sort::test_files0_from_nul ... ok",
+ "test test_sort::test_files0_from_non_utf8_name ... ok",
+ "test test_sort::test_files0_from_nul2 ... ok",
+ "test test_sort::test_files0_from_zero_length ... ok",
+ "test test_sort::test_files0_read_error ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35348",
+ "lockMergeOrCompletionAnchor": "bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "officialAdvisory": {
+ "sha256": "649a788e0db24a89b2a8e26978948ed76971d4e7a7cecb7cc0bb2d7b751a47c5",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35348.json"
+ },
+ "officialCodeCommits": [
+ "bb8f7095fd63206ff54a4fb4256375e9e1ad88c9"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/11593",
+ "securityBoundary": {
+ "control": "NUL\u5206\u9694\u6a94\u540d\u4ea4\u7d66os_string_from_vec\uff1bLinux/Unix\u5206\u652f\u76f4\u63a5OsString::from_vec\uff0c\u4e0d\u4ee5UTF8 expect\u89f8\u767cpanic\u3002\u5f8c\u7e8c6e286332\u7684\u6e05\u55ae\u6a94\u540d\u4fee\u88dc\u4ea6\u662f0.10\u7956\u5148\uff1b\u76ee\u524d\u5be6\u4f5c\u70ba\u7b49\u50f9raw-byte helper\u3002",
+ "platform": "native Linux amd64",
+ "sink": "sort\u7a0b\u5e8fpanic\uff0f\u7ba1\u7dda\u4e2d\u65b7",
+ "source": "--files0-from\u7684\u975eUTF8\u6a94\u540d\u6216\u6e05\u55ae\u8def\u5f91"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 2036,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 2030,
+ "path": "src/uu/sort/src/sort.rs",
+ "sha256": "95820b55661c4281b3a51e54496fb9c0f5050ed202f1e8c68b87efb3e17de5dd",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/sort/src/sort.rs"
+ },
+ {
+ "lineEnd": 516,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 510,
+ "path": "src/uucore/src/lib/lib.rs",
+ "sha256": "81d80403e7e16eb030c209f79cb65c2c5ad03f65457f07822fab42e9882c3f84",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uucore/src/lib/lib.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 866,
+ "behind_by": 0,
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "mergeBase": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "status": "ahead",
+ "total_commits": 866,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/bd170a5d4b19ed985417d322dffb8cade2cbbad5...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_cp::test_cp_preserve_setuid_when_chown_succeeds ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35350",
+ "lockMergeOrCompletionAnchor": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "officialAdvisory": {
+ "sha256": "a01cef4bc2418a58a044257522edf3d5e004155a78d96f9103a9047a3a5f9602",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35350.json"
+ },
+ "officialCodeCommits": [
+ "bd170a5d4b19ed985417d322dffb8cade2cbbad5"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12135",
+ "securityBoundary": {
+ "control": "\u8a18\u9304ownership_preserved=false\uff0c\u4fdd\u7559mode\u524d\u6e05\u96640o6000\uff1bUbuntu cp quilt\u4fdd\u7559\u6b64\u5206\u652f\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u8907\u672c\u932f\u8aa4\u4fdd\u7559setuid/setgid",
+ "source": "cp -p\u7121\u6cd5\u4fdd\u7559\u539fowner"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 1853,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1847,
+ "path": "src/uu/cp/src/cp.rs",
+ "sha256": "7094892394071883367c3b0de3b1b7d0beee6a354d32931397808276547ae525",
+ "ubuntuQuiltTouches": [
+ {
+ "name": "cp-fix-symlink-target-permissions.patch",
+ "sha256": "53cfbb7a0376da6710985496774f8981e71d207aa97d932f73d5978c978ea100"
+ }
+ ],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/cp/src/cp.rs"
+ },
+ {
+ "lineEnd": 1870,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1864,
+ "path": "src/uu/cp/src/cp.rs",
+ "sha256": "7094892394071883367c3b0de3b1b7d0beee6a354d32931397808276547ae525",
+ "ubuntuQuiltTouches": [
+ {
+ "name": "cp-fix-symlink-target-permissions.patch",
+ "sha256": "53cfbb7a0376da6710985496774f8981e71d207aa97d932f73d5978c978ea100"
+ }
+ ],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/cp/src/cp.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 1078,
+ "behind_by": 0,
+ "commit": "874efa7cc3361cb5af2a97db869147f910bcab44",
+ "mergeBase": "874efa7cc3361cb5af2a97db869147f910bcab44",
+ "status": "ahead",
+ "total_commits": 1078,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/874efa7cc3361cb5af2a97db869147f910bcab44...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_mv::test_mv_cross_device_preserves_ownership ... ok",
+ "test test_mv::test_mv_cross_device_preserves_ownership_recursive ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35351",
+ "lockMergeOrCompletionAnchor": "874efa7cc3361cb5af2a97db869147f910bcab44",
+ "officialAdvisory": {
+ "sha256": "a63b5f1b5ad9b023c5fdc155effa5051639039be12ee201b7388de1a154ed096",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35351.json"
+ },
+ "officialCodeCommits": [
+ "874efa7cc3361cb5af2a97db869147f910bcab44"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/11706",
+ "securityBoundary": {
+ "control": "\u4e00\u822c\u6a94\u3001symlink\u8207\u76ee\u9304\u905e\u8ff4\u8def\u5f91\u5747\u547c\u53ebpreserve_ownership\uff0c\u5f9esource metadata\u53d6uid/gid\u4e26\u7528\u4e0dfollow symlink\u7684chown\uff1b\u4fdd\u7559\u5408\u6cd5\u975eroot best-effort\u8a9e\u610f\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u642c\u79fb\u5f8cowner\u932f\u7f6e",
+ "source": "mv\u8de8\u6a94\u6848\u7cfb\u7d71copy/delete fallback"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 1470,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1464,
+ "path": "src/uu/mv/src/mv.rs",
+ "sha256": "852b6f61b68bc9b96ead38f1132ae23047a14e4229705a8f11f31f5ba1608734",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mv/src/mv.rs"
+ },
+ {
+ "lineEnd": 1441,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1435,
+ "path": "src/uu/mv/src/mv.rs",
+ "sha256": "852b6f61b68bc9b96ead38f1132ae23047a14e4229705a8f11f31f5ba1608734",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mv/src/mv.rs"
+ },
+ {
+ "lineEnd": 1287,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1281,
+ "path": "src/uu/mv/src/mv.rs",
+ "sha256": "852b6f61b68bc9b96ead38f1132ae23047a14e4229705a8f11f31f5ba1608734",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mv/src/mv.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 649,
+ "behind_by": 0,
+ "commit": "c004672c5eb2415b03f11f766583c4b967ba024f",
+ "mergeBase": "c004672c5eb2415b03f11f766583c4b967ba024f",
+ "status": "ahead",
+ "total_commits": 649,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/c004672c5eb2415b03f11f766583c4b967ba024f...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_mkfifo::test_mkfifo_permission_unchanged_when_failed ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35352",
+ "lockMergeOrCompletionAnchor": "c004672c5eb2415b03f11f766583c4b967ba024f",
+ "officialAdvisory": {
+ "sha256": "57f20a07444990dbe8a5736abc10b0173f9aeaaf0bc6599ba69df5bdfade2ccd",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35352.json"
+ },
+ "officialCodeCommits": [
+ "c004672c5eb2415b03f11f766583c4b967ba024f"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12174",
+ "securityBoundary": {
+ "control": "\u6307\u5b9amode\u96a8mkfifoat\u5efa\u7acb\u539f\u5b50\u5957\u7528\uff1b\u76ee\u524dutility\u7121\u5f8c\u7e8cpath-based set_permissions\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u5c0d\u975e\u9810\u671f\u76ee\u6a19chmod",
+ "source": "mkfifo\u5efa\u7acb\u8207\u5f8c\u7e8cpath chmod\u9593\u7684symlink\u7f6e\u63db"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 77,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 71,
+ "path": "src/uu/mkfifo/src/mkfifo.rs",
+ "sha256": "6c7c49a911cff80e6ea0499044971bd232c37b298a5a0c83551b41d5a9f46dbe",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mkfifo/src/mkfifo.rs"
+ },
+ {
+ "lineEnd": 152,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 146,
+ "path": "src/uu/mkfifo/src/mkfifo.rs",
+ "sha256": "6c7c49a911cff80e6ea0499044971bd232c37b298a5a0c83551b41d5a9f46dbe",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mkfifo/src/mkfifo.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 793,
+ "behind_by": 0,
+ "commit": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "mergeBase": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "status": "ahead",
+ "total_commits": 793,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/aa30c103004463ffe7b8a98ca0ce519d56bbbd41...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [],
+ "proofGap": "\u672c\u8f2a\u53ea\u4f5csource closure\uff1b\u4e0d\u5c07\u4e00\u822csuite PASS\u5192\u5145\u6b64CVE\u5c08\u5c6c\u52d5\u614b\u91cd\u73fe\u5168\u8986\u84cb\u3002",
+ "reused": true
+ },
+ "id": "CVE-2026-35354",
+ "lockMergeOrCompletionAnchor": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "officialAdvisory": {
+ "sha256": "f2499baee7cf751fab8bac7328bd54186608a4485452650d2ab0d7634e06eb15",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35354.json"
+ },
+ "officialCodeCommits": [
+ "500dd65c539bff848c712bf7a8a111109a32eae9"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12170",
+ "securityBoundary": {
+ "control": "copy\u671f\u9593\u6301\u6709source/dest fd\uff0ccopy_xattrs_fd\u4ee5FileExt list/get/set\u64cd\u4f5c\u540cinode\uff1bignore-unsupported wrapper\u53ea\u6539ENOTSUP\u7b56\u7565\uff0c\u672a\u6539\u56depath-based xattr\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u76ee\u7684\u6a94\u5b89\u5168xattr\u6df7\u96dc",
+ "source": "mv\u8de8\u6a94\u6848\u7cfb\u7d71xattr\u8907\u88fd\u6642\u7684path\u7f6e\u63db"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 1431,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1425,
+ "path": "src/uu/mv/src/mv.rs",
+ "sha256": "852b6f61b68bc9b96ead38f1132ae23047a14e4229705a8f11f31f5ba1608734",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mv/src/mv.rs"
+ },
+ {
+ "lineEnd": 62,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 56,
+ "path": "src/uucore/src/lib/features/fsxattr.rs",
+ "sha256": "305e34986037e15dbbd05b2a2bc497527433af1f06f078f290642cee9c42eae6",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uucore/src/lib/features/fsxattr.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 866,
+ "behind_by": 0,
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "mergeBase": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "status": "ahead",
+ "total_commits": 866,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/bd170a5d4b19ed985417d322dffb8cade2cbbad5...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_cp::test_cp_final_mode_unchanged_after_restrictive_create ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35357",
+ "lockMergeOrCompletionAnchor": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "officialAdvisory": {
+ "sha256": "38f682a70a2dbae9144fe748b26558e8928103d43b660021d5f2a4f37417afef",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35357.json"
+ },
+ "officialCodeCommits": [
+ "7c8563d88ffa8f0dba5745587baad0d0c58de62d"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12135",
+ "securityBoundary": {
+ "control": "Linux\u5404copy/reflink/sparse/stream\u8def\u5f91\u4f7f\u7528create_dest_restrictive\uff1b\u5171\u7528helper\u65bcopen\u5efa\u7acb\u6642\u4ee50600\u5957\u7528\uff0c\u4e4b\u5f8c\u624d\u653e\u5bec\u81f3\u5408\u6cd5\u6700\u7d42mode\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u65b0\u6a94\u5167\u5bb9\u66ab\u6642\u6d29\u6f0f",
+ "source": "cp\u65b0\u76ee\u7684\u6a94\u5efa\u7acb\u5230\u6536\u7dcamode\u524d\u7684\u8b80\u53d6\u7af6\u614b"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 41,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 35,
+ "path": "src/uu/cp/src/platform/linux.rs",
+ "sha256": "c05c36f1f8cc3a904b4666b86d4719f42887354d6d7b67027b38f546c2dac86e",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/cp/src/platform/linux.rs"
+ },
+ {
+ "lineEnd": 79,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 73,
+ "path": "src/uucore/src/lib/features/safe_copy.rs",
+ "sha256": "638d5ca70fae2ac677fe154a6302f76a760bcaa370194e1b5123951d81ffa54e",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uucore/src/lib/features/safe_copy.rs"
+ },
+ {
+ "lineEnd": 84,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 78,
+ "path": "src/uucore/src/lib/features/safe_copy.rs",
+ "sha256": "638d5ca70fae2ac677fe154a6302f76a760bcaa370194e1b5123951d81ffa54e",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uucore/src/lib/features/safe_copy.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 866,
+ "behind_by": 0,
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "mergeBase": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "status": "ahead",
+ "total_commits": 866,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/bd170a5d4b19ed985417d322dffb8cade2cbbad5...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [],
+ "proofGap": "\u672c\u8f2a\u53ea\u4f5csource closure\uff1b\u4e0d\u5c07\u4e00\u822csuite PASS\u5192\u5145\u6b64CVE\u5c08\u5c6c\u52d5\u614b\u91cd\u73fe\u5168\u8986\u84cb\u3002",
+ "reused": true
+ },
+ "id": "CVE-2026-35359",
+ "lockMergeOrCompletionAnchor": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "officialAdvisory": {
+ "sha256": "7ada9773bfc6935e73c9fe554c10c3ad6118cd202e9073039a357817cd45d7f5",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35359.json"
+ },
+ "officialCodeCommits": [
+ "acadaa0e51ab82309ec81e6021d630b875cd3e10"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12135",
+ "securityBoundary": {
+ "control": "cp\u4f9ddereference policy\u7522\u751fnofollow\uff0cLinux\u666e\u901a/reflink/sparse/stream\u4f86\u6e90open\u90fd\u50b3\u905e\u6b64\u503c\uff1bsafe_copy::open_source\u8a2dO_NOFOLLOW\uff0c\u6b63\u5e38dereference\u4ecd\u4fdd\u7559GNU\u8a9e\u610f\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u610f\u5916\u8b80\u53d6\u4efb\u610f\u654f\u611fsymlink\u76ee\u6a19",
+ "source": "cp no-dereference\u4f86\u6e90\u5728lstat\u8207open\u9593\u88ab\u63db\u6210symlink"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 2870,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 2864,
+ "path": "src/uu/cp/src/cp.rs",
+ "sha256": "7094892394071883367c3b0de3b1b7d0beee6a354d32931397808276547ae525",
+ "ubuntuQuiltTouches": [
+ {
+ "name": "cp-fix-symlink-target-permissions.patch",
+ "sha256": "53cfbb7a0376da6710985496774f8981e71d207aa97d932f73d5978c978ea100"
+ }
+ ],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/cp/src/cp.rs"
+ },
+ {
+ "lineEnd": 39,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 33,
+ "path": "src/uu/cp/src/platform/linux.rs",
+ "sha256": "c05c36f1f8cc3a904b4666b86d4719f42887354d6d7b67027b38f546c2dac86e",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/cp/src/platform/linux.rs"
+ },
+ {
+ "lineEnd": 56,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 50,
+ "path": "src/uucore/src/lib/features/safe_copy.rs",
+ "sha256": "638d5ca70fae2ac677fe154a6302f76a760bcaa370194e1b5123951d81ffa54e",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uucore/src/lib/features/safe_copy.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 659,
+ "behind_by": 0,
+ "commit": "d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "mergeBase": "d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "status": "ahead",
+ "total_commits": 659,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/d296f8d39f3a71ef7c5bd3b949e65aa978f1436d...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_touch::test_touch_does_not_truncate_symlink_target ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35360",
+ "lockMergeOrCompletionAnchor": "d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "officialAdvisory": {
+ "sha256": "66d7237bdb5f70d308eae0b07b3fbcee9dc565a2d992420d1495df61ff8507ef",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35360.json"
+ },
+ "officialCodeCommits": [
+ "d296f8d39f3a71ef7c5bd3b949e65aa978f1436d"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12171",
+ "securityBoundary": {
+ "control": "\u7f3a\u6a94\u5efa\u7acb\u5206\u652f\u6539\u547c\u53ebcreate_without_truncate\uff0cOpenOptions\u7684truncate(false)\u907f\u514d\u6e05\u7a7a\u65e2\u6709\u76ee\u6a19\uff1b\u4fdd\u7559\u6b63\u5e38\u5efa\u7acb\u8207timestamp\u529f\u80fd\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u65e2\u6709\u6a94\u6848\u88ab\u622a\u65b7",
+ "source": "touch\u5224\u5b9a\u7f3a\u6a94\u5f8c\u7684path\uff0fsymlink\u7f6e\u63db"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 449,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 443,
+ "path": "src/uu/touch/src/touch.rs",
+ "sha256": "5a348cc8d276aa0613393ea0451fca703d5e614ae83e6cae6b25582cf0cbda27",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/touch/src/touch.rs"
+ },
+ {
+ "lineEnd": 507,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 501,
+ "path": "src/uu/touch/src/touch.rs",
+ "sha256": "5a348cc8d276aa0613393ea0451fca703d5e614ae83e6cae6b25582cf0cbda27",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/touch/src/touch.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 956,
+ "behind_by": 0,
+ "commit": "6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "mergeBase": "6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "status": "ahead",
+ "total_commits": 956,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/6b16cc9688fe998f5dc2786f53f22ceea709e318...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_rm::test_dot_protection_multiple_slashes ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35363",
+ "lockMergeOrCompletionAnchor": "6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "officialAdvisory": {
+ "sha256": "3ad4ea99fd41baf62cfe13313fab3a96611eadf1dec973cd7e162e52761454b2",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35363.json"
+ },
+ "officialCodeCommits": [
+ "6b16cc9688fe998f5dc2786f53f22ceea709e318"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/11005",
+ "securityBoundary": {
+ "control": "\u4fdd\u8b77\u6aa2\u67e5\u5148\u79fb\u9664\u6240\u6709\u5c3e\u7aefseparator\uff0c\u518d\u5224\u65b7\u6700\u5f8ccomponent\u662f\u5426.\u6216..\uff1b\u522a\u9664\u6d41\u7a0b\u5148\u547c\u53eb\u6b64guard\u3002",
+ "platform": "native Linux amd64",
+ "sink": "current/parent\u76ee\u9304\u5167\u5bb9\u88ab\u8aa4\u522a",
+ "source": "rm -rf\u7684./\u3001.///\u3001../\u7b49\u8def\u5f91"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 811,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 805,
+ "path": "src/uu/rm/src/rm.rs",
+ "sha256": "776b445347e3087e90d811b38b5b7604c8e362608c1772b762aa7aabe55671c4",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/rm/src/rm.rs"
+ },
+ {
+ "lineEnd": 974,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 968,
+ "path": "src/uu/rm/src/rm.rs",
+ "sha256": "776b445347e3087e90d811b38b5b7604c8e362608c1772b762aa7aabe55671c4",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/rm/src/rm.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 793,
+ "behind_by": 0,
+ "commit": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "mergeBase": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "status": "ahead",
+ "total_commits": 793,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/aa30c103004463ffe7b8a98ca0ce519d56bbbd41...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_mv::test_mv_cross_device_refuses_planted_symlink_dest ... ok",
+ "test test_mv::test_mv_cross_device_dir_refuses_symlink_at_recreated_dest ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35364",
+ "lockMergeOrCompletionAnchor": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "officialAdvisory": {
+ "sha256": "fe0f4e59872e1e27a8a00dd28b97574e7db1e3659abf6254649db5ca9ccc7921",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35364.json"
+ },
+ "officialCodeCommits": [
+ "500dd65c539bff848c712bf7a8a111109a32eae9"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12170",
+ "securityBoundary": {
+ "control": "\u8de8device\u666e\u901a\u6a94fallback\u4ee5create_dest_restrictive(to,true)\u8a2dO_NOFOLLOW\uff1bfd\u4fdd\u7559\u5230copy\u8207metadata\u8655\u7406\uff0csource/dest\u4e0d\u56e0\u518dopen\u5c0e\u5411symlink\u76ee\u6a19\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u4efb\u610f\u76ee\u6a19\u88ab\u622a\u65b7\uff0f\u8986\u5beb",
+ "source": "mv\u79fb\u9664\u820a\u76ee\u7684\u6a94\u5f8c\u88ab\u690d\u5165symlink"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 1424,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 1418,
+ "path": "src/uu/mv/src/mv.rs",
+ "sha256": "852b6f61b68bc9b96ead38f1132ae23047a14e4229705a8f11f31f5ba1608734",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/mv/src/mv.rs"
+ },
+ {
+ "lineEnd": 79,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 73,
+ "path": "src/uucore/src/lib/features/safe_copy.rs",
+ "sha256": "638d5ca70fae2ac677fe154a6302f76a760bcaa370194e1b5123951d81ffa54e",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uucore/src/lib/features/safe_copy.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 763,
+ "behind_by": 0,
+ "commit": "acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "mergeBase": "acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "status": "ahead",
+ "total_commits": 763,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/acd51ef18d90e5c26f694df721f896fa1c4a9edc...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [],
+ "proofGap": "\u672c\u8f2a\u53ea\u4f5csource closure\uff1b\u4e0d\u5c07\u4e00\u822csuite PASS\u5192\u5145\u6b64CVE\u5c08\u5c6c\u52d5\u614b\u91cd\u73fe\u5168\u8986\u84cb\u3002",
+ "reused": true
+ },
+ "id": "CVE-2026-35367",
+ "lockMergeOrCompletionAnchor": "acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "officialAdvisory": {
+ "sha256": "84d14cafbb0fb2c3e353af964db234cab2ec91bd836f357938718836d3fd77d5",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35367.json"
+ },
+ "officialCodeCommits": [
+ "acd51ef18d90e5c26f694df721f896fa1c4a9edc"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12339",
+ "securityBoundary": {
+ "control": "OpenOptions\u65bc\u5efa\u7acb\u6642\u660e\u78bamode(0600)\uff0c\u65e2\u6709\u6a94mode\u4e0d\u88ab\u6539\u5beb\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u9810\u8a2dworld-readable\u8f38\u51fa",
+ "source": "nohup\u65b0\u5efanohup.out"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 166,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 160,
+ "path": "src/uu/nohup/src/nohup.rs",
+ "sha256": "93a8ab0c82728d3856b0e181312bdb3f0418d86a33f70af00b61e915e2b3173c",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/nohup/src/nohup.rs"
+ },
+ {
+ "lineEnd": 174,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 168,
+ "path": "src/uu/nohup/src/nohup.rs",
+ "sha256": "93a8ab0c82728d3856b0e181312bdb3f0418d86a33f70af00b61e915e2b3173c",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/nohup/src/nohup.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 765,
+ "behind_by": 0,
+ "commit": "f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "mergeBase": "f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "status": "ahead",
+ "total_commits": 765,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [],
+ "proofGap": "\u672c\u8f2a\u53ea\u4f5csource closure\uff1b\u4e0d\u5c07\u4e00\u822csuite PASS\u5192\u5145\u6b64CVE\u5c08\u5c6c\u52d5\u614b\u91cd\u73fe\u5168\u8986\u84cb\u3002",
+ "reused": true
+ },
+ "id": "CVE-2026-35368",
+ "lockMergeOrCompletionAnchor": "f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "officialAdvisory": {
+ "sha256": "cf6665f83960cfa5fee8ef279acc8173d0686de8fde2b7aafc64a7577e23d90a",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35368.json"
+ },
+ "officialCodeCommits": [
+ "f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/11211",
+ "securityBoundary": {
+ "control": "\u5404userspec branch\u5728enter_chroot\u524d\u89e3\u6790name\u2192uid/gid\u4e26\u8655\u7406supplementary gids\uff0c\u4e4b\u5f8c\u624dchroot\u53cadrop uid/gid\uff0c\u4e0d\u5728\u4e0d\u53ef\u4fe1root\u5167\u4ee5root\u67e5NSS\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u4e0d\u53ef\u4fe1NEWROOT\u7684NSS\u6a21\u7d44\u5728root\u6b0a\u9650\u8f09\u5165",
+ "source": "chroot --userspec\u642d\u914d\u4e0d\u53ef\u4fe1NEWROOT"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 431,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 398,
+ "path": "src/uu/chroot/src/chroot.rs",
+ "sha256": "9282c030345eee3c85c029b0978f587452087c4c873d3ed2f527c4aa677b4e7c",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/chroot/src/chroot.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 636,
+ "behind_by": 0,
+ "commit": "48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "mergeBase": "48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "status": "ahead",
+ "total_commits": 636,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/48d030dfb2ca7659ee2735f09cda706c8245a3fc...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_ln::test_symlink_target_dir_non_utf8_source_name ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35373",
+ "lockMergeOrCompletionAnchor": "48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "officialAdvisory": {
+ "sha256": "fdab95750b279926855844eea7863db1034ae790d9b0cb76c26f59067dd98b73",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35373.json"
+ },
+ "officialCodeCommits": [
+ "48d030dfb2ca7659ee2735f09cda706c8245a3fc"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/11403",
+ "securityBoundary": {
+ "control": "\u4f86\u6e90\u4fdd\u6301PathBuf\uff0fOsStr\uff0c\u4f7f\u7528srcpath.file_name()\u53cajoin(srcpath)\uff0c\u4e0d\u8981\u6c42to_str\u6210\u529f\uff1b\u5408\u6cd5Unix raw-byte\u6a94\u540d\u53ef\u901a\u904e\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u975e\u9810\u671f\u5931\u6557\uff0flocal DoS",
+ "source": "ln target-directory\u5f62\u5f0f\u7684\u975eUTF8\u4f86\u6e90\u6a94\u540d"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 352,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 346,
+ "path": "src/uu/ln/src/ln.rs",
+ "sha256": "5c681cfa583a268bdce9a9b740f9dcd8b38c5ab53754883e5c20800259a4f1b3",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/ln/src/ln.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 646,
+ "behind_by": 0,
+ "commit": "58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "mergeBase": "58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "status": "ahead",
+ "total_commits": 646,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/58040feb5c66fce588a7ffac299b3a23bd55cd81...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [],
+ "proofGap": "source unit regression\u5b58\u5728\uff0c\u4f46\u672a\u5728\u65e2\u6709strict log\u89c0\u5bdf\u5230\u6b64test\u540d\uff1b\u6b64\u6536\u64da\u4e0d\u65b0\u589e\u6e2c\u8a66\u6216\u5ba3\u7a31\u8a72unit\u5df2\u57f7\u884c\u3002",
+ "reused": true
+ },
+ "id": "CVE-2026-35374",
+ "lockMergeOrCompletionAnchor": "58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "officialAdvisory": {
+ "sha256": "6d611d8423834ed4e58c0a2dda5b8d4123f88d6059f4a511144a0163bbad3bd4",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35374.json"
+ },
+ "officialCodeCommits": [
+ "58040feb5c66fce588a7ffac299b3a23bd55cd81"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12565",
+ "securityBoundary": {
+ "control": "\u65b0\u5efa\u5148create_new\uff1b\u65e2\u6709\uff0f\u91cd\u958boutput\u7528\u5df2open fd\u7684inode\u8207input\u4f5c\u8eab\u5206\u6bd4\u8f03\uff0c\u901a\u904e\u5f8c\u624dset_len(0)\u6216\u7e7c\u7e8c\u5beb\uff1b\u5f8c\u7e8c\u5408\u6cd5\u975eseekable\u8f38\u51fa\u76f8\u5bb9\u5206\u652f\u672a\u64a4\u56de\u8eab\u5206\u6aa2\u67e5\u3002",
+ "platform": "native Linux amd64",
+ "sink": "input\u88ab\u7af6\u614b\u622a\u65b7\uff0f\u8986\u5beb",
+ "source": "split output\u8def\u5f91\u5728\u9810\u6aa2\u5f8c\u7f6e\u63db\u70bainput alias"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 196,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 170,
+ "path": "src/uu/split/src/platform/unix.rs",
+ "sha256": "9c0a3fe7abdf46d00ef8468ce19495ed687689b7d4abc329a5f63d551b497021",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/split/src/platform/unix.rs"
+ },
+ {
+ "lineEnd": 159,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 153,
+ "path": "src/uu/split/src/platform/unix.rs",
+ "sha256": "9c0a3fe7abdf46d00ef8468ce19495ed687689b7d4abc329a5f63d551b497021",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/split/src/platform/unix.rs"
+ },
+ {
+ "lineEnd": 210,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 204,
+ "path": "src/uu/split/src/platform/unix.rs",
+ "sha256": "9c0a3fe7abdf46d00ef8468ce19495ed687689b7d4abc329a5f63d551b497021",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/split/src/platform/unix.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ },
+ {
+ "additionalLockCommits": [],
+ "ancestry": [
+ {
+ "ahead_by": 518,
+ "behind_by": 0,
+ "commit": "b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "mergeBase": "b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "status": "ahead",
+ "total_commits": 518,
+ "url": "https://api.github.com/repos/uutils/coreutils/compare/b9d8ff64c224eb425702584b66ec0ac64a99592a...28b6856d7b215bf844b4223589cb54ade84f5223?per_page=1"
+ }
+ ],
+ "confidence": "high",
+ "effectiveRevertFound": false,
+ "evidence": "\u5b98\u65b9\u4fee\u88dc\uff0f\u7b49\u50f9\u5f8c\u7e8c\u5b89\u5168\u63a7\u5236\u5b58\u5728\u65bcexact Ubuntu0.10 orig+quilt\uff1bnative custom patch\u53ea\u52d5id\uff0c\u4e0d\u5f71\u97ff\u672c18\u9805\u3002\u6b64fixed\u50c5\u8868\u793a\u672cqualified component source\uff0c\u672a\u4fee\u6539\u4efb\u4f55\u6210\u54c1VEX\u6216canonical\u3002",
+ "existingNativeLog": {
+ "completeCveSpecificDynamicCoverageClaimed": false,
+ "observedRelatedTests": [
+ "test test_env::test_split_string_single_quotes_keep_unknown_backslash_sequences_literal ... ok"
+ ],
+ "proofGap": null,
+ "reused": true
+ },
+ "id": "CVE-2026-35377",
+ "lockMergeOrCompletionAnchor": "b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "officialAdvisory": {
+ "sha256": "f47ecf4fa958906c580316e18badce6c90c1ba5fbaff01b7fbc71f0700c1ae1f",
+ "url": "https://ubuntu.com/security/cves/CVE-2026-35377.json"
+ },
+ "officialCodeCommits": [
+ "55298ee943231c07e944bd6e1a9cf9e01236d324"
+ ],
+ "officialPullRequest": "https://github.com/uutils/coreutils/pull/12657",
+ "securityBoundary": {
+ "control": "state_single_quoted\u7684escape\u5206\u652f\u5c0d\u672a\u77e5\u5b57\u5143\u4fdd\u7559literal BACKSLASH\u800c\u975e\u56de\u50b3invalid-sequence\u3002\u771f\u6b63code commit\u662fPR12657\u516755298ee9\uff0clock\u7684b9d8\u662f\u8a72\u5df2merged PR\u5b8c\u6210\u9328\u9ede\uff08\u672ccommit\u53ea\u6539GNU test patch\uff09\u3002",
+ "platform": "native Linux amd64",
+ "sink": "\u5408\u6cd5split-string\u4ee5125\u975e\u9810\u671f\u7d42\u6b62",
+ "source": "env -S\u55ae\u5f15\u865f\u5167\u672a\u77e5\u53cd\u659c\u7dda\u5e8f\u5217"
+ },
+ "sourceEvidence": [
+ {
+ "lineEnd": 288,
+ "lineMeaning": "1-based lines in exact Ubuntu orig + original enabled quilt, before the id-only custom patch",
+ "lineStart": 244,
+ "path": "src/uu/env/src/split_iterator.rs",
+ "sha256": "da0cc908efed1adbf9baace843cfa4de72317512ba4143141ddd0a031bae21a9",
+ "ubuntuQuiltTouches": [],
+ "upstreamOriginalFileUrl": "https://github.com/uutils/coreutils/blob/28b6856d7b215bf844b4223589cb54ade84f5223/src/uu/env/src/split_iterator.rs"
+ }
+ ],
+ "sourceFixPresent": true,
+ "status": "fixed",
+ "successorCommit": null,
+ "ubuntuQuiltConclusion": "\u539fseries\u6240\u6709patch target\u5747\u5df2\u6838\u5c0d\uff1b\u6d89\u53ca\u6b64\u63a7\u5236\u7684source hunks\u5df2\u5728\u8a18\u61b6\u9ad4\u4ee5\u5b8c\u6574exact context\u91cd\u5efa\u4e26\u9010\u6bb5\u6aa2\u67e5\uff0c\u672a\u64a4\u56de\u76ee\u524d\u5b89\u5168\u63a7\u5236\u3002",
+ "verdictScope": "qualified-component-source-closure-only"
+ }
+ ],
+ "schemaVersion": 1,
+ "scope": {
+ "canonicalOrProductDispositionsChanged": false,
+ "componentPackage": "rust-coreutils_0.10.0-1ubuntu2~26.04.1+pasturestack.1_amd64.deb",
+ "customVersion": "0.10.0-1ubuntu2~26.04.1+pasturestack.1",
+ "newIdCvesExcludedFromReassessment": [
+ "CVE-2026-35370",
+ "CVE-2026-35371"
+ ],
+ "originalPendingCves": 18,
+ "verdict": "18\u9805\u5728\u6307\u5b9a\u5408\u683ccandidate\u7684source\u4e2d\u6709\u5b98\u65b9\u4fee\u5fa9\uff0f\u7b49\u50f9\u5b89\u5168\u5f8c\u7e8c\u5be6\u4f5c\uff0c\u672a\u898b\u6709\u6548revert\uff1b\u4e0d\u662f20\u9805\u7522\u54c1\u5df2\u4fee\u5ba3\u544a\u3002"
+ },
+ "sourceBuildBoundary": {
+ "customPatch": {
+ "commit": "ff382f527994b8a021ec0a086dec4c5cafa34f2a",
+ "firstFixedVersion": "0.11.0",
+ "fixedCves": [
+ "CVE-2026-35370",
+ "CVE-2026-35371"
+ ],
+ "name": "ff382f527994b8a021ec0a086dec4c5cafa34f2a.patch",
+ "onlyTargetPaths": [
+ "src/uu/id/src/id.rs",
+ "tests/by-util/test_id.rs"
+ ],
+ "original18SourceUntouched": true,
+ "paths": [
+ "src/uu/id/src/id.rs",
+ "tests/by-util/test_id.rs"
+ ],
+ "pullRequest": "https://github.com/uutils/coreutils/pull/14103",
+ "sha256": "32d843876e0814cb070104afc049c036391604ea517c6e837ffc9345fe1e2de2",
+ "size": 4082,
+ "url": "https://github.com/uutils/coreutils/commit/ff382f527994b8a021ec0a086dec4c5cafa34f2a.patch"
+ },
+ "helperOnlyNonSecurityChanges": "\u539f\u5305\u88ddchangelog\u52a0first-party\u7248\u672c\uff0f\u4f86\u6e90\uff1bstrict target\u7531\u5916\u90e8native-tests.mk\u57f7\u884c\uff0c\u672a\u653918\u9805\u5b89\u5168source\u3002",
+ "nativeHelper": "server/security/ubuntu-backports/rust-coreutils/build-rust-coreutils-backport.py",
+ "preBuildSourcePins": {
+ "lines": [
+ 343,
+ 347
+ ],
+ "meaning": "\u5b98\u65b9dsc\u4f86\u6e90\u9a57\u8b49\u8207dpkg-source orig+quilt\u62bd\u53d6\u5f8cbaseline SHA\u6838\u5c0d"
+ },
+ "producerCommit": "07e4a7afa827cb45514a72aba12f03b1d30d121d"
+ },
+ "status": "PASS_ORIGINAL18_SOURCE_CLOSURE",
+ "summary": {
+ "consumerBuildsOrRuntimeTestsPerformed": 0,
+ "inconclusiveSourceClosure": 0,
+ "oneJsonReceiptOnly": true,
+ "productCvesClosed": 0,
+ "sourceClosed": 18,
+ "stillVulnerable": 0
+ },
+ "ubuntuQuilt": {
+ "enabledPatches": [
+ {
+ "allTargetPaths": [
+ "GNUmakefile"
+ ],
+ "patch": "fix-prefix.diff",
+ "reviewedPathsTouched": [
+ "GNUmakefile"
+ ],
+ "sha256": "b6257d84300a1013651f94646911433f6d1b606159511f76513b4e74834da064"
+ },
+ {
+ "allTargetPaths": [
+ "Cargo.toml"
+ ],
+ "patch": "workspace-exclude.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "05ae74382b1217fe344f55ace401d2c742ece04b11a2c6e0b40de4ae628139c4"
+ },
+ {
+ "allTargetPaths": [
+ "GNUmakefile"
+ ],
+ "patch": "use-l10n-translations-in-makefile.patch",
+ "reviewedPathsTouched": [
+ "GNUmakefile"
+ ],
+ "sha256": "26cf17de79dbd5f6bd14ed1b5c9fcd84353e046a191197d2a140ed0d3f847ed5"
+ },
+ {
+ "allTargetPaths": [
+ "GNUmakefile",
+ "src/uucore/src/lib/mods/locale.rs"
+ ],
+ "patch": "fix-locale-path.patch",
+ "reviewedPathsTouched": [
+ "GNUmakefile"
+ ],
+ "sha256": "f24b39c73345332368f887ccdff4dfb16cebfaee87837488fb723e5b1cbb0631"
+ },
+ {
+ "allTargetPaths": [
+ "src/bin/uudoc.rs"
+ ],
+ "patch": "improve-man.diff",
+ "reviewedPathsTouched": [],
+ "sha256": "feafc91daa75d1895d85c46a4efc8ba6b1e4366f9fd0b05ecede9ede29b6fcb8"
+ },
+ {
+ "allTargetPaths": [
+ "src/bin/uudoc.rs"
+ ],
+ "patch": "reproducible-man-date.diff",
+ "reviewedPathsTouched": [],
+ "sha256": "5426f02e0117aa33d4a28768b040ccd94297211cd3cbaa374b57736d0ebd0b4a"
+ },
+ {
+ "allTargetPaths": [
+ "Cargo.toml"
+ ],
+ "patch": "remove-workspace-members.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "3f60549df336e397f56dcdd8adf0c488ba13b40c6a6060ab19b32f13cfddfd5a"
+ },
+ {
+ "allTargetPaths": [
+ "Cargo.toml"
+ ],
+ "patch": "Tweak-release-build-profile.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "0fbf669c40b7ec4880a9eea78dee14c041d1a07b0d39dde4bb69bf1ee852d085"
+ },
+ {
+ "allTargetPaths": [
+ "Cargo.toml"
+ ],
+ "patch": "build-stty.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "d869d308d06cc9ccad6ab93f5e21297a8434711d286ec17a513ea76d38b5ac41"
+ },
+ {
+ "allTargetPaths": [
+ "src/bin/coreutils.rs",
+ "tests/by-util/test_env.rs",
+ "tests/test_util_name.rs"
+ ],
+ "patch": "require-utility-to-be-invoked-at-matching-path.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "6761e8474f7244fa3e9e5d35038c0939aa8e34e32a2beb7254b90757a803640f"
+ },
+ {
+ "allTargetPaths": [
+ "src/uu/dd/src/dd.rs",
+ "tests/by-util/test_dd.rs"
+ ],
+ "patch": "dd-ensure-full-writes.patch",
+ "reviewedPathsTouched": [
+ "src/uu/dd/src/dd.rs"
+ ],
+ "sha256": "dd2c5a3cc8dfb89dea54e68db55903859f09ab6f0bb6ff3e7cbf7c7ecc7f80b1"
+ },
+ {
+ "allTargetPaths": [
+ "Cargo.toml"
+ ],
+ "patch": "rustix-use-libc-backend.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "29e6bd79d75a68cd6263c0f00fed61ed32c9e3270b3d32b86abd9a8b0e0c0cc8"
+ },
+ {
+ "allTargetPaths": [
+ "src/uu/df/src/df.rs",
+ "src/uu/df/src/filesystem.rs"
+ ],
+ "patch": "df-statfs-fallback.patch",
+ "reviewedPathsTouched": [],
+ "sha256": "23ee682dcf37befeefd4b6f52fe56de4d6756cf831f728acc8003c0f871beb43"
+ },
+ {
+ "allTargetPaths": [
+ "src/uu/cp/src/cp.rs",
+ "tests/by-util/test_cp.rs"
+ ],
+ "patch": "cp-fix-symlink-target-permissions.patch",
+ "reviewedPathsTouched": [
+ "src/uu/cp/src/cp.rs",
+ "tests/by-util/test_cp.rs"
+ ],
+ "sha256": "53cfbb7a0376da6710985496774f8981e71d207aa97d932f73d5978c978ea100"
+ }
+ ],
+ "inMemoryReplayOnly": true,
+ "matchedNativeBaselineFilePins": 18,
+ "originalRulesAndPackagingPreserved": true,
+ "seriesSha256": "90890b8dda501e3212c826d070e42a9107e5a600cfe48fd3143b14e79f031a9b"
+ },
+ "validation": {
+ "nativeSourcePinsMatched": 18,
+ "newBuildTestScanExecuted": false,
+ "officialCommitPatchByteBindings": 20,
+ "originalCves": 18,
+ "originalUbuntuQuiltSourceControlsReviewed": true,
+ "producerLockBytesMatch": true,
+ "rawPackageShaMatched": true,
+ "sourceOrCanonicalModified": false,
+ "status": "PASS",
+ "ubuntuOrigEqualsFreshOfficialArchive": true,
+ "uniqueLockOfficialAnchors": 16
+ }
+ },
+ "originalReceiptSha256": "73189028622c0a0fc074067b4c5b8521094921531f95cf3e68073baaebd2fbbd",
+ "projection": {
+ "excludedFields": [
+ "identity.originalZip"
+ ],
+ "reason": "Omit the local evidence-store reference; preserve all source, native identity and limitation evidence."
+ },
+ "schemaVersion": 1
+}
diff --git a/server/security/ubuntu-backports/bundle/source-inputs.lock.json b/server/security/ubuntu-backports/bundle/source-inputs.lock.json
new file mode 100644
index 0000000000..1dfaa8eb01
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/source-inputs.lock.json
@@ -0,0 +1,698 @@
+{
+ "bundleVersion": "0.1.0",
+ "consumerFixture": {
+ "baselineMainIndexEvidence": {
+ "inReleaseSha256": "45f95ce276cdba3e41870516a130e03c58b8b7a79e9546b0efe9e526d255740c",
+ "index": {
+ "path": "main/binary-amd64/Packages.xz",
+ "sha256": "ed9ac41cb263efaecc5a06a0742dc56570e2fb5ff94c4f2e4b2fbdcddd73cf24",
+ "size": 1480032
+ },
+ "packages": [
+ {
+ "Filename": "pool/main/c/coreutils-from/coreutils-from-gnu_0.0.0~ubuntu25_all.deb",
+ "Package": "coreutils-from-gnu",
+ "SHA256": "862be75c7dd5d92815f2b80149bbcf819d11d21b1dba734f62b545bc5acb0eb7",
+ "Size": "6902",
+ "Version": "0.0.0~ubuntu25"
+ },
+ {
+ "Filename": "pool/main/c/coreutils-from/coreutils-from-uutils_0.0.0~ubuntu25_all.deb",
+ "Package": "coreutils-from-uutils",
+ "SHA256": "fd89943a93fdb3e2746faff8fe1bb23106dbdd6c891df0f50fe2f8b42d6f089d",
+ "Size": "7124",
+ "Version": "0.0.0~ubuntu25"
+ },
+ {
+ "Filename": "pool/main/c/coreutils/gnu-coreutils_9.7-3ubuntu2_amd64.deb",
+ "Package": "gnu-coreutils",
+ "SHA256": "6b5b60a5c372b6e9d1dcfa1507317aae59bf809d4f4d6d363d3ef0a58a189137",
+ "Size": "1339676",
+ "Version": "9.7-3ubuntu2"
+ }
+ ],
+ "snapshot": "20261023T000000Z",
+ "suite": "resolute"
+ },
+ "signatureVerification": "The fixture apt-get update requires the original Ubuntu archive keyring and verifies signed InRelease; local review matched the downloaded Packages.xz SHA/size to that InRelease.",
+ "snapshotURL": "https://snapshot.ubuntu.com/ubuntu/20261023T000000Z",
+ "ubuntuImage": "ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b"
+ },
+ "image": "ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0",
+ "nativeInputs": {
+ "glibc": {
+ "artifactId": 12573643405,
+ "artifactName": "ubuntu-native-glibc-507cadcb3eaafcd6a85c453f2f180a8aef415d88-40122645083",
+ "ciClosure": {
+ ".github/workflows/ubuntu-backport-native.yml": {
+ "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79",
+ "nativeSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79",
+ "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files."
+ },
+ "server/security/ubuntu-backports/Dockerfile.native-ci": {
+ "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b",
+ "nativeSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b",
+ "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files."
+ }
+ },
+ "copyright": {
+ "file": "COPYRIGHT",
+ "sha256": "c31d10900fdb95d1889005145a2205bb75e3bc018d3ec8265b81364901748e09"
+ },
+ "copyrightEvidence": {
+ "path": "debian/copyright",
+ "source": {
+ "name": "glibc_2.43-2ubuntu2.4.debian.tar.xz",
+ "sha256": "28103a7cf808c29901c6053d89a4e8299880abfbb850c0d97a09c8df8533e306",
+ "size": 530764,
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.debian.tar.xz"
+ }
+ },
+ "evidence": {
+ "COPYRIGHT": "c31d10900fdb95d1889005145a2205bb75e3bc018d3ec8265b81364901748e09",
+ "builder-packages.tsv": "fa9d46ddbb27235e5b3f6f29fbbc63e0a5a5bfeb681ed57302b14938640dc385",
+ "generated-debian-control": "a1cca0ba6356e1318f218b05dec1213bae7d43dce2bd79055119d75070a9b85e",
+ "glibc.lock.json": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12",
+ "glibc_2.43-2ubuntu2.4+pasturestack.1.dsc": "4a7bd217e000e44933db0b5d48d04e94936ea188dd7a42c4cc8ce6eca8f209b5",
+ "glibc_2.43-2ubuntu2.4+pasturestack.1_amd64.buildinfo": "ddc397c4caf06be75cc7c4be8394212f1ddfb242c115136f22868b1f3a5198b7",
+ "glibc_2.43-2ubuntu2.4+pasturestack.1_amd64.changes": "9bcf4cb463052e74fd08e649998397d9bc60220dd3c32ed16cccc20f560e0997",
+ "manifest.tsv": "a3ca5de5aec3bcb88c2499486ff14d9f976ce58a81a2a6d3daa7c76e099c0869",
+ "native-config.make": "4d7257d3ab6788b9a28cc7948e9025470996dd751eac2c91e37ec3e13977c0b9",
+ "native-configparms": "4b09bdaed30ef3d58e957d4599420019f2ca31c6dba952ae7e62084b89557b09",
+ "provenance.json": "cc77cdde3f1b6b5459b45282bbbdcccbe42c3d3fd5d7a41f4623a92509a2263a",
+ "tests.sum": "4d9b149fd0711f4a4cf67c15fae7f5be23edabf3f40f0c542125eda0c804e19f",
+ "tst-fopen-ccs-empty.out": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "tst-fopen-ccs-empty.test-result": "818ea455ca8c5e31c156a6b7046bf1a552bbcb9432d147c6a87c9a64669e57e4",
+ "tst-fopenloc.out": "89a245c0599fd89e6a9b8536c79ea7aefe16203cb4332cb8a841cdff4a5818d6",
+ "tst-fopenloc.test-result": "5234ae2c930bcfcbd094eab622797a4f869bbe261c9326e6a6f380656676c45f",
+ "tst-fopenloc2.out": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "tst-fopenloc2.test-result": "80339c05fd3053b631971af004613f6da2be7a4ca39454d1d173d35a7693b13a"
+ },
+ "fixedVulnerabilities": [
+ "CVE-2026-18374"
+ ],
+ "jobId": 120957337330,
+ "lockSha256": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12",
+ "packages": [
+ {
+ "file": "libc6_2.43-2ubuntu2.4+pasturestack.1_amd64.deb",
+ "name": "libc6",
+ "sha256": "4119ebc8610d17694db4100e6838a693f8927189523f4b4be32be5aa4a82f497",
+ "source": "glibc",
+ "version": "2.43-2ubuntu2.4+pasturestack.1"
+ },
+ {
+ "file": "libc-bin_2.43-2ubuntu2.4+pasturestack.1_amd64.deb",
+ "name": "libc-bin",
+ "sha256": "6e8ea949edd167a48e4f2ded82f4117e4dca89fc7c8060062b614bbb682b9417",
+ "source": "glibc",
+ "version": "2.43-2ubuntu2.4+pasturestack.1"
+ },
+ {
+ "file": "libc-gconv-modules-extra_2.43-2ubuntu2.4+pasturestack.1_amd64.deb",
+ "name": "libc-gconv-modules-extra",
+ "sha256": "22cb6a83267d6e873538983d1eafda6cac9240c5c18ff61e2652fd6ae982e374",
+ "source": "glibc",
+ "version": "2.43-2ubuntu2.4+pasturestack.1"
+ }
+ ],
+ "runConclusion": "success",
+ "runId": 40122645083,
+ "runtimePackages": [
+ "libc6",
+ "libc-bin",
+ "libc-gconv-modules-extra"
+ ],
+ "sourceCommit": "507cadcb3eaafcd6a85c453f2f180a8aef415d88",
+ "sourceFiles": {
+ "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2",
+ "server/security/ubuntu-backports/glibc/build-glibc-backport.py": "a086f51359bab0d0994070d4cdf0e4fcc1f19bc4acf8dbd71d189707c72ef3e6",
+ "server/security/ubuntu-backports/glibc/glibc.lock.json": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12",
+ "server/security/ubuntu-backports/glibc/test_glibc_backport.py": "d08cb0b93d6c49c0124eb69cedd55af3949b41c48a09c57d52d49e304641a583"
+ },
+ "status": "qualified",
+ "vexScope": "Exact official glibc 2.43 fix and regression backports; native amd64 full upstream summary retains actual XFAIL/UNSUPPORTED/XPASS states. Original Ubuntu packaging templates generate first-party control metadata; these packages are not issued by Canonical.",
+ "zipSha256": "ed6714b9b4a884a0c0462b84b01f5d7f9e08b4cc1bbf849b521ac43fa4ef36c4"
+ },
+ "jq": {
+ "artifactId": 12570170631,
+ "artifactName": "ubuntu-native-jq-bc129a88a5ec8c4e64b3ed89bfd97c7e82362d7c-40116776455",
+ "ciClosure": {
+ ".github/workflows/ubuntu-backport-native.yml": {
+ "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79",
+ "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."
+ },
+ "server/security/ubuntu-backports/Dockerfile.native-ci": {
+ "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b",
+ "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."
+ }
+ },
+ "copyright": {
+ "file": "COPYRIGHT",
+ "sha256": "92965c1a25cfced63db0fcc869d060d773d6105da9bd8d3da232ed1691cda778"
+ },
+ "evidence": {
+ "COPYRIGHT": "92965c1a25cfced63db0fcc869d060d773d6105da9bd8d3da232ed1691cda778",
+ "base64test.log": "0ec2e6651d00c82646424e5512bba5f4ec8376f5aab2306133f5a906009a97d9",
+ "base64test.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "builder-packages.tsv": "379d4a07715d6a587d9ce25105bc216dca2dd08ec8faa61f3ce0d7470b21d225",
+ "jq.lock.json": "3a000a74debdf3d04d0e475f7413f82e2aa422cb95d888c4fe1ee3949536c229",
+ "jq_1.8.2-0pasturestack1~26.04.1_amd64.buildinfo": "9a601a1e4fb6e0730b0d9bf740604b0e077d952b023f245d128a6a6429e9ccb4",
+ "jq_1.8.2-0pasturestack1~26.04.1_amd64.changes": "af908c5e73271510dc042a61c6e5917d91ab59dc976a8362c19aca10359967d2",
+ "jqtest.log": "5ab9b68330470807e9ce0587d7b89e7f0aba17d54a3c767cdf074093fdc77874",
+ "jqtest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "manifest.tsv": "2a3b62801f5f94ad88fa97c2372bd5831d5286c49d5b0f66c4a80cbc5c329ce1",
+ "manonigtest.log": "ae377728ce527be518b204e01ca59b897a86e37f339f4b896f1f477e1f98da24",
+ "manonigtest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "mantest.log": "3e9312bd0c98b88e2abe7c3d9225db33b6d89ba40b8ed906e1f2a845fe9528df",
+ "mantest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "onigtest.log": "26f99a726b6a9999f034abd0428a658db1c3dc5f4afe422eb8ae33f739d0d05e",
+ "onigtest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "optionaltest.log": "f87d529edd1dd85b887b3556f46ea034a6973468ce4fd30b4702ffe3792b7ff0",
+ "optionaltest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "provenance.json": "673cf3ebcb16ddd769f31b14960b4d7cfb091ae6f28e810d60050d4eb56c3c1d",
+ "shtest.log": "43247569114fac8222645dcd376fa4d8c146ce30bc9547e9eb4893fd5050406d",
+ "shtest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "uritest.log": "676ab96470f36d878725abb2f7d820c7098b26130409b8e964586b699816500a",
+ "uritest.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229",
+ "utf8test.log": "4a603cd14e39c981b121c56d1436279deca1493b2c36d9731391708d8b32b098",
+ "utf8test.trs": "7f175f2f5d04511903d382671ba96ca623e127f57730d53966587cf2c2689229"
+ },
+ "fixedVulnerabilities": [
+ "CVE-2026-32316",
+ "CVE-2026-33947",
+ "CVE-2026-33948",
+ "CVE-2026-39956",
+ "CVE-2026-39979",
+ "CVE-2026-40164",
+ "CVE-2026-40612",
+ "CVE-2026-41256",
+ "CVE-2026-41257",
+ "CVE-2026-43894",
+ "CVE-2026-43895",
+ "CVE-2026-43896",
+ "CVE-2026-44777",
+ "CVE-2026-47770",
+ "CVE-2026-49839",
+ "CVE-2026-54679",
+ "GHSA-gf4g-95wj-4q4r",
+ "GHSA-hj52-j2c9-r8r4"
+ ],
+ "jobId": 120939862251,
+ "lockSha256": "3a000a74debdf3d04d0e475f7413f82e2aa422cb95d888c4fe1ee3949536c229",
+ "packages": [
+ {
+ "file": "jq_1.8.2-0pasturestack1~26.04.1_amd64.deb",
+ "name": "jq",
+ "sha256": "88dce8936c13daa0195783836e5b8658d25545068b59a8e8232ae8db442dcea9",
+ "source": "jq",
+ "version": "1.8.2-0pasturestack1~26.04.1"
+ },
+ {
+ "file": "libjq1_1.8.2-0pasturestack1~26.04.1_amd64.deb",
+ "name": "libjq1",
+ "sha256": "e1b61456c00ae0508406163000d1a816de0c64e4db9f268a9667f31c4315f1df",
+ "source": "jq",
+ "version": "1.8.2-0pasturestack1~26.04.1"
+ }
+ ],
+ "runConclusion": "failure",
+ "runId": 40116776455,
+ "runtimePackages": [
+ "jq",
+ "libjq1"
+ ],
+ "sourceCommit": "bc129a88a5ec8c4e64b3ed89bfd97c7e82362d7c",
+ "sourceFiles": {
+ "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2",
+ "server/security/ubuntu-backports/jq/build-jq-release.py": "c71512fbb65df44c75d75cb47d31fa338acaca7b14699b9fcb419ea64180694b",
+ "server/security/ubuntu-backports/jq/jq.lock.json": "3a000a74debdf3d04d0e475f7413f82e2aa422cb95d888c4fe1ee3949536c229",
+ "server/security/ubuntu-backports/jq/test_jq_release.py": "8c78f324d76956b892e9b4f8cf19d17c5462df413a20826de39e80ded52018a5"
+ },
+ "status": "qualified",
+ "vexScope": "Pinned official jq 1.8.2 release source with original Ubuntu packaging and system libonig.so.5; exact qualified package bytes only. This statement does not cover the separate libonig package.",
+ "zipSha256": "5536c059d0db1fd9587650634a9f1ec9800b804768c8ae4f83cbab19effcaef1"
+ },
+ "libxrender": {
+ "artifactId": 12571296848,
+ "artifactName": "ubuntu-native-libxrender-07e4a7afa827cb45514a72aba12f03b1d30d121d-40120642373",
+ "ciClosure": {
+ ".github/workflows/ubuntu-backport-native.yml": {
+ "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79",
+ "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."
+ },
+ "server/security/ubuntu-backports/Dockerfile.native-ci": {
+ "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b",
+ "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."
+ }
+ },
+ "copyright": {
+ "file": "COPYRIGHT",
+ "sha256": "ed2a78123a5076b4f11da38cb1ffb85b5cb9fdc26b5746c517b1d7e7a9029704"
+ },
+ "evidence": {
+ "COPYRIGHT": "ed2a78123a5076b4f11da38cb1ffb85b5cb9fdc26b5746c517b1d7e7a9029704",
+ "builder-packages.tsv": "5d5f3d65439a54d9680fb20acc2bcb06a7efadbd9187fbce5a7d43ed716803c2",
+ "libxrender.lock.json": "6ca89ea2cca1c36a7db42adf24c8ae8fff5fa69ef853da3508c74a38151fa720",
+ "libxrender_0.9.12-1build1+pasturestack.1_amd64.buildinfo": "a80211c4a875b07c67cc61fb4d341ee928063f73e416cab17031594354f0c647",
+ "libxrender_0.9.12-1build1+pasturestack.1_amd64.changes": "29847cb93b70b3e9bc69561f6dd2fec3803c59e8df3d1fd87f3780b68e363b3c",
+ "manifest.tsv": "d9a1e6c23df97346a4c3d69cf37559906699978e9b04d08cbb224ee06aae21ea",
+ "owning-check.log": "b5eb8d8721131e3f5ab58b6132a53f7690331cf26f9779772df64c8738ca9e28",
+ "provenance.json": "bf9592ac26e82fffc2c896e06cbb04b53a1f8970c7b96bd0f79d2b62a6db2e52",
+ "reply-boundary.json": "c50ae165a3fbfaf03eaa96daf64e3943e85cf19b7a51bb2c97365a87b709624f"
+ },
+ "fixedVulnerabilities": [
+ "CVE-2026-88807"
+ ],
+ "jobId": 120951219154,
+ "lockSha256": "6ca89ea2cca1c36a7db42adf24c8ae8fff5fa69ef853da3508c74a38151fa720",
+ "packages": [
+ {
+ "file": "libxrender1_0.9.12-1build1+pasturestack.1_amd64.deb",
+ "name": "libxrender1",
+ "sha256": "49fa0140dcc46481271fb203c2b4bf7a4c9b588bd849f498856c75a4926c212c",
+ "source": "libxrender",
+ "version": "1:0.9.12-1build1+pasturestack.1"
+ }
+ ],
+ "runConclusion": "success",
+ "runId": 40120642373,
+ "runtimePackages": [
+ "libxrender1"
+ ],
+ "sourceCommit": "07e4a7afa827cb45514a72aba12f03b1d30d121d",
+ "sourceFiles": {
+ "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2",
+ "server/security/ubuntu-backports/libxrender/build-libxrender-backport.py": "343166b9a540fe9e134f5b22c02bd6faf776359267c76885957814fd53f66c8d",
+ "server/security/ubuntu-backports/libxrender/libxrender.lock.json": "6ca89ea2cca1c36a7db42adf24c8ae8fff5fa69ef853da3508c74a38151fa720",
+ "server/security/ubuntu-backports/libxrender/reply-boundary.c": "486cf9c93b149623be3470a52158d407acfccf07b42f398740d60a955c90885f",
+ "server/security/ubuntu-backports/libxrender/test_libxrender_backport.py": "76cb352603055d61574997d5c526604a37a864ee78775fd9d5087ddec8254986",
+ "server/security/ubuntu-backports/source_helpers.py": "d2b3f310f8ed99a50a696fe14094b7117ec5d7ed576e6080d5540c0e82891486"
+ },
+ "status": "qualified",
+ "vexScope": "Exact official QueryFormats reply-boundary patch; original-source negative control and patched/packaged controls passed. Upstream make check defines zero testcases; the separate native fixture supplies this evidence.",
+ "zipSha256": "5d58962ad638f0562d72584a263f7fa3e4bafb34473fedd19e892b07eba5896b"
+ },
+ "p11-kit": {
+ "artifactId": 12570718592,
+ "artifactName": "ubuntu-native-p11-kit-2a47f3cd99b6afc47b54ee6d8837b834e95c50f2-40121338859",
+ "ciClosure": {
+ ".github/workflows/ubuntu-backport-native.yml": {
+ "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79",
+ "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."
+ },
+ "server/security/ubuntu-backports/Dockerfile.native-ci": {
+ "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b",
+ "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."
+ }
+ },
+ "copyright": {
+ "file": "COPYRIGHT",
+ "sha256": "a5611fbad151d1fa0aa484f6c393a5166af33ce9a9783dfb2712d6b87b462b37"
+ },
+ "evidence": {
+ "COPYRIGHT": "a5611fbad151d1fa0aa484f6c393a5166af33ce9a9783dfb2712d6b87b462b37",
+ "builder-packages.tsv": "33900d45599ada520e85af10a65664c16011240e8e72c23280d7e0a5168a66fa",
+ "manifest.tsv": "3395b9f859643f84a4dfb656ec60c66199cbb75ed8eaad45468f72147eb9bd73",
+ "p11-kit.lock.json": "636bfbe8d9730e1838207edd9e3a6108f373d601c5b13591888913c8a042d046",
+ "p11-kit_0.26.2-2+pasturestack.1_amd64.buildinfo": "7c109845d877822003710a8332fae788305f2f1dff05bc43060b254b7b99f2bf",
+ "p11-kit_0.26.2-2+pasturestack.1_amd64.changes": "b92d9b76536cb7e1c77ae5e6289dd65ddf8a0fe2b388fc53cf851794858cd7ac",
+ "p11-kit__test-export-public.sh.log": "ac7f88758b57f814b8520981e87e452918c9bf12cb896a4fe9097ab96d963efa",
+ "p11-kit__test-export-public.sh.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "p11-kit__test-generate-keypair.sh.log": "3a887affe49f6fcec98c79393db9773a69b1590aba750f03d75dcb8ff37b356c",
+ "p11-kit__test-generate-keypair.sh.trs": "5dddb0d74ac9fd4ce3f50299d3ec68dce52ca4ce8332d2e59c5355b5da536bab",
+ "p11-kit__test-import-public.sh.log": "ae529f425541c579763902a30bc575f634706260860d5226512099a68e498bd7",
+ "p11-kit__test-import-public.sh.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "p11-kit__test-list-mechanisms.sh.log": "a257e893105e69a857e1e29d65e3d474a1dff3715305f3965ce28c5b76169159",
+ "p11-kit__test-list-mechanisms.sh.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "p11-kit__test-list-tokens.sh.log": "72a907acf2f3c9af79a84b2c262c00532ad31897c860ea0fce3bd73b0ef317dc",
+ "p11-kit__test-list-tokens.sh.trs": "5dddb0d74ac9fd4ce3f50299d3ec68dce52ca4ce8332d2e59c5355b5da536bab",
+ "p11-kit__test-lists.sh.log": "2a04a7e73255bda2fa884b9910adeab7176e50362e216b28f4f62a96398820a1",
+ "p11-kit__test-lists.sh.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "p11-kit__test-messages.sh.log": "c1caa148cdf02cd146d6708a861b8ed27d023aae164f53f417f07cd97ddebeb6",
+ "p11-kit__test-messages.sh.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "p11-kit__test-objects.sh.log": "836efa2f43826a25b650c8bdab7cb261da852cae11c89bd056dfcdb016a814fd",
+ "p11-kit__test-objects.sh.trs": "9cdbbc7152714b8ec44ad6627c09d2670e17b4393c1424c33387dd355aa1f425",
+ "p11-kit__test-profiles.sh.log": "9e5e71a99f71c28797288b40ae24b99a43f27787a4a8223472921508efcac0e9",
+ "p11-kit__test-profiles.sh.trs": "eaad2ad90ee6daadee5c9bb5486426cac231c29639133c5f18db189f97657c36",
+ "p11-kit__test-server.sh.log": "a87077f31cc42ff50842f9f51b5eb6faf8cb71ba3663334440cbd79f9563d405",
+ "p11-kit__test-server.sh.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "provenance.json": "b5294407c8361f8cfee55c1a460e51d4b0b2db24c0584b68f114fcd49842ac9e",
+ "test-argv.log": "ba614cff4234d49a95c48953449cfe688cda19017947d57199b8d836bdeea248",
+ "test-argv.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "test-array.log": "8178fe96de4acab0277208e9f21cbeeb7c8ccd8a1abfd6039a6373cbb020c058",
+ "test-array.trs": "c5bc74eab7054de88905f106fe54ce55fdae1c98f39b6eef9ee569891892f671",
+ "test-asn1.log": "42653f6c15cc9bcf969fa566a261679c6b67f8d899d2105ac414f16097ce88c7",
+ "test-asn1.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "test-attrs.log": "d4c7736ff7e80df07beb32a9a1907ba666f84b1f40983164c9e031ff67a741e5",
+ "test-attrs.trs": "a74ca7b7925c8181aeb6bb89ba3d8d315e820e1f5791e4f8c8d1008223aa4ce3",
+ "test-base64.log": "fd3ff98b7f186230e7b384649b804a7985e362ced5d455f44889c8d0a2ba1443",
+ "test-base64.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-buffer.log": "4a36bf5a0b8f1f37d61666e14c5c6374d28c7b45ee6be6e3cf58da41ed6bf288",
+ "test-buffer.trs": "9fcb93a25d29bf85271d24531b3740047d85d130da7ca1229752b86f6e3d9bc6",
+ "test-builder.log": "23619c173eab7be3b84b449ee863b05ce770750740ee4811cf603d32727402e8",
+ "test-builder.trs": "db443166649c920d8ef64482844285d53b504c0fdaf17ce13e1a6d7e9c13f502",
+ "test-bundle.log": "fe6906b28fd8c411a451c7532153780307f2403b9232ab062dcb2d098388f73c",
+ "test-bundle.trs": "9fcb93a25d29bf85271d24531b3740047d85d130da7ca1229752b86f6e3d9bc6",
+ "test-cer.log": "4d345ec15a728c4018015a16c8f528031f4d88214c4f43e3f39cef8524ed2ed6",
+ "test-cer.trs": "7b0d14d75180a40ebe2164a52b0e32f921fcbb447789f3aeb74f14c44263d50b",
+ "test-compat.log": "d042e1aefc0bb3a77397633ae44155065e91f9a844b25c6851a8bc426f71aa58",
+ "test-compat.trs": "5dddb0d74ac9fd4ce3f50299d3ec68dce52ca4ce8332d2e59c5355b5da536bab",
+ "test-conf.log": "e0f377dff98faa96c4534684e032c4dc3da098e78c1141057750ff6d9ae1ee31",
+ "test-conf.trs": "e3b1a457cf69962eaaa6b89eebf9bbf2fb24d90e62ca50668aa87ed4f2861653",
+ "test-constants.log": "6b2329e8888f7c20a77749f928689f00546483238d5846efdcb02777baba0c86",
+ "test-constants.trs": "f81624a4f492eb26078518866962fe6dd9a2c0c34529f97a9a54c075fd3622fc",
+ "test-deprecated.log": "97bfa253e8a3f6c9d402ff556166ae37d0c7520bf09b7212a466ff8f67dce4d3",
+ "test-deprecated.trs": "0cf6c4dbcae9310529416ae5d56fbafcf873d48959e154c2af8c46307a9712af",
+ "test-dict.log": "7ae1c4db184023e036c19c1359b1e4879f49c1e5a7d567509f27b28addc58f7f",
+ "test-dict.trs": "c63c2276cf4ec02bcf4b9e1c7fa282491c6e7c07c0e51ab278f84c07bf4b802f",
+ "test-digest.log": "b1d91d3ac7e899314f2cd674458d1e52c158611605f5586fafbde4ec6220f322",
+ "test-digest.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "test-edk2.log": "37a926fff731f901cc421710120a149a113874899a5baba0daeda6323feac951",
+ "test-edk2.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "test-enumerate.log": "30006e3235ba30c08ce8e664913d7293842fb051b75c123c13e28147d87d7989",
+ "test-enumerate.trs": "035c9327d0173a24a63239d44c9fe044d246e9d67e77d66ecbf2ab2ae8d7d69b",
+ "test-filter.log": "f9e9b1ca738fad000134d1e233c05db25b41aeafe7323056cb806efdb8fbaff5",
+ "test-filter.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "test-hash.log": "b6b90fbfeee8f21e96c8e6e58e958daef4bf9f99c8518f2ca5deabe11826b372",
+ "test-hash.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-hex.log": "232d52eeae73f85471bec913f17981d1a5ff8e887d37378223065fb99435a5a2",
+ "test-hex.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-index.log": "828dce6d499442cbd50c56ce38da1817b4594901a8a93c316d1ea8d3ea82d374",
+ "test-index.trs": "e72c652859efdf9c3717475dd7e90d06bfa3d2d4293a194dc1f0a3137a37d619",
+ "test-init.log": "7f16b3676c995cd0f005689cfe557a22a4aa88fc85041f1a313bd92b4b577fc0",
+ "test-init.trs": "7b247c4c3ece016e82481ce77af6db795cae3cab861dc4fc86c788efdda8a3a1",
+ "test-iter.log": "65603e0db48b3346f723380be8d90c84747ddac71c3153e08062210cced300a5",
+ "test-iter.trs": "a4d55c0b1ad9d0455d354c02016ebf5a9e9a6fd8d907bc7f499c6e799bb7b405",
+ "test-jks.log": "65fa6d45a00e4358c10d7ff82aaad2a03216a82cd7eee6661fcc25d99458e5ae",
+ "test-jks.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-lexer.log": "400aadffd290caf89782d61a4025e205c184cfdabe2375094d5b59b15ca032a2",
+ "test-lexer.trs": "9fcb93a25d29bf85271d24531b3740047d85d130da7ca1229752b86f6e3d9bc6",
+ "test-log.log": "29c42e483a30644759bd7646d3fa888500b740fff4517229ab3fb328e5087187",
+ "test-log.trs": "0dc694b2fb33f367e8543148ffb838de67c98057b7fde9b51aac989bf50b06db",
+ "test-log3.log": "c159f5901e08d1e14ce1493cafbe5cacd5bf01bb259cefae61e3ed1ef01fbcc2",
+ "test-log3.trs": "566219f61afb12349258aecfd7e9dab8cf2cf610a0ef8f49c50ff097d51ebb0b",
+ "test-managed.log": "fdf3efebb5684841e4d22b1afd73ecdcc705f49ed7bdbf5c13844a4418f7e57f",
+ "test-managed.trs": "566219f61afb12349258aecfd7e9dab8cf2cf610a0ef8f49c50ff097d51ebb0b",
+ "test-message.log": "c40622436bdf1f03e155fc7e3cdaa02647497510d8510ec7aa6381b174a092df",
+ "test-message.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "test-module.log": "6f85ff1ff45dedce717977381fd320524347abed77f77d13cb76d0c98899e73e",
+ "test-module.trs": "7c2ec063bdfd3b3d491c85d4f0526d0eea0c03a7b6691bf4dd0a4a977ea43253",
+ "test-modules.log": "fb13d8f465cdb4ebbe88052d38c33f86ba3eafad80031200483b50a62619e887",
+ "test-modules.trs": "02d3912701f201986b92e087ce92a7114eaf6ff8266e84cc663047dc02250acd",
+ "test-oid.log": "f489119f8cecbadd3b9def6c586154253c7d7cdc1306f3f35de22b4e1f7d2921",
+ "test-oid.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-openssl.log": "4cf9743a9591af378e33c3c6247eeb1f06c2b7db1a01ec2690daa9b7c0d01966",
+ "test-openssl.trs": "f81624a4f492eb26078518866962fe6dd9a2c0c34529f97a9a54c075fd3622fc",
+ "test-parser.log": "ab4f96f220acc874a8dc166e7a4b645feb83b36edd25c0fedb3f0be19f985e09",
+ "test-parser.trs": "9cdbbc7152714b8ec44ad6627c09d2670e17b4393c1424c33387dd355aa1f425",
+ "test-path.log": "0d2103ab73dbb624f66a4076476e9e2d870f9ab671040fe80d44d2cdeaaeec37",
+ "test-path.trs": "0cf6c4dbcae9310529416ae5d56fbafcf873d48959e154c2af8c46307a9712af",
+ "test-pem.log": "d3306c15ab4c133b74ff1b1abba7db04dd84ac28216ca8c83977cf0c3c66613b",
+ "test-pem.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "test-persist.log": "39a9f4dfdb8021f1407c1d2ea6f1d1b9d1e1965a9b2ba2a3dbfda40f32ef0441",
+ "test-persist.trs": "d2e39ee331ca5994ec02ee5a7c6fd81ce13e9b5e7a47c3a068c93accee2d60a1",
+ "test-pin.log": "7721b10dd207f90b9be2bb1432028fa8657dbf73ca0ccfc765c805115c88a1c9",
+ "test-pin.trs": "c5bc74eab7054de88905f106fe54ce55fdae1c98f39b6eef9ee569891892f671",
+ "test-progname.log": "8f516cac7e35bd11066dd25b713c771f6a6935179e00e30de1625a180c9b653e",
+ "test-progname.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-proxy.log": "314c4051f95a3b7825c74a1fa5167bc71622ffa1768e9325157d78a5fc863c2e",
+ "test-proxy.trs": "a4f4e1d89f20ce646a51f23ac3f879513dafaf8c15a918d773253333b0378cd7",
+ "test-proxy3.log": "86e2b6637ff95d769cff86c71fa846c3f6cb80d3d7ae69c6ec45f63315c3635e",
+ "test-proxy3.trs": "a4f4e1d89f20ce646a51f23ac3f879513dafaf8c15a918d773253333b0378cd7",
+ "test-rpc-message.log": "cd162cb2a5edfbe4e805310e82f5c81a0bc0ddd419a099138578975b4a345397",
+ "test-rpc-message.trs": "5c1774bdb1c10f6d4e1acd63c903490e414770d83508a8b4abbc692174a7d125",
+ "test-rpc.log": "1389c4801d837c836bef39fb5b1028d61ce1cb2f080cb297513aa8b6179fe95b",
+ "test-rpc.trs": "f8e5da746cd392aeb0bc57ed56e234c30c4edffd80462b2f651b2f5a9e412580",
+ "test-runtime.log": "6d33d72e781c0d6ce763a3469aa7d7a909717b26846ab8313b6651985ac4d8dd",
+ "test-runtime.trs": "09416818c720b3bad5e7e2667490ad2e6075f8846f2346a7822a300f4a7bb6a2",
+ "test-save.log": "52ce41229d526dca5d8d65bbefdea70bb51bb051c285a0515603800e1ee6b7ab",
+ "test-save.trs": "ab29310fbe2d1eb904fa3fd371782c5f77c1b3dec1a1c73fb7914fe1228095ce",
+ "test-server.log": "722074c08613ba082a76a6c7f3f8822d87033d2c21ef791d920ce0e131f4759f",
+ "test-server.trs": "c5bc74eab7054de88905f106fe54ce55fdae1c98f39b6eef9ee569891892f671",
+ "test-suite.log": "15414b28374f07200d23e21f3dc48fe0a22eed760c2b02878190ce6e8d8adff9",
+ "test-tests.log": "7f1cd1e5e1082751f51eaef70ec836f7a91ceccd69dcdb0eb383e32c26102eb3",
+ "test-tests.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "test-token.log": "1aaea97a0bab0c17d71b1b5f66aaba39987cebe2ba6f48e071654aef419eb243",
+ "test-token.trs": "d2e39ee331ca5994ec02ee5a7c6fd81ce13e9b5e7a47c3a068c93accee2d60a1",
+ "test-transport.log": "569fe841c68fb26c7506ce385c513f0fe2ea87cd06bb2e1be324e44e644b80bd",
+ "test-transport.trs": "566219f61afb12349258aecfd7e9dab8cf2cf610a0ef8f49c50ff097d51ebb0b",
+ "test-transport3.log": "58c296b59b01a7e3aff8b17b5052d9ca746c59e4f291e74ea48124c210fd9052",
+ "test-transport3.trs": "3844f258e6e12ee69624cc3b922166335b113800b783f01e43d38cd698b16424",
+ "test-uri.log": "53efc49e9d232706628f29f62ebc8fea5db59d20243f4a0eb3207f13650379ef",
+ "test-uri.trs": "74be51fa3d5f510d2083c7f0b824c5863b539d5f0ab7a18782a790c938c9f74f",
+ "test-url.log": "4a7443d85db41082d80767e49f2f510452f890ae26f6b9ecf63b27c6d4f0a9c2",
+ "test-url.trs": "9fcb93a25d29bf85271d24531b3740047d85d130da7ca1229752b86f6e3d9bc6",
+ "test-utf8.log": "07e208f404e4c9f57909f11acabedf21498f8ec8ba0ee7d290cfb6bc20dc2d7f",
+ "test-utf8.trs": "9fcb93a25d29bf85271d24531b3740047d85d130da7ca1229752b86f6e3d9bc6",
+ "test-util.log": "c85bea9491f2f213490ca365446cb71f91cb6a79e02cf48cec8978199bb0ece9",
+ "test-util.trs": "6b72d26b78d92ec48ec59376b0dd9497ce01545f4ce9d56a41b64a3a91740153",
+ "test-version.log": "54a1a2ded34663e602d3f65fbf886b01a00c2baf7e181373263d31fd82899484",
+ "test-version.trs": "976ed8fdf324eb7f8f7501ea3ce1abeaa3000b30c96b3a7291b0d421a10200b4",
+ "test-virtual.log": "525f71d96b36b464a02f6d4687ce4298625815f17675c6b2ad4ba46bfaacb49f",
+ "test-virtual.trs": "5dddb0d74ac9fd4ce3f50299d3ec68dce52ca4ce8332d2e59c5355b5da536bab",
+ "test-x509.log": "cfff462f44fc206e184027488801d0ae8a6a677ed0a710826efeefe86b8c4ad5",
+ "test-x509.trs": "7b247c4c3ece016e82481ce77af6db795cae3cab861dc4fc86c788efdda8a3a1"
+ },
+ "fixedVulnerabilities": [
+ "CVE-2026-13757",
+ "CVE-2026-18938"
+ ],
+ "jobId": 120953326544,
+ "lockSha256": "636bfbe8d9730e1838207edd9e3a6108f373d601c5b13591888913c8a042d046",
+ "packages": [
+ {
+ "file": "libp11-kit0_0.26.2-2+pasturestack.1_amd64.deb",
+ "name": "libp11-kit0",
+ "sha256": "b4d244aed0607da60c6c2da716fae1d66edc4a59449d5b598df613b84069702d",
+ "source": "p11-kit",
+ "version": "0.26.2-2+pasturestack.1"
+ },
+ {
+ "file": "p11-kit_0.26.2-2+pasturestack.1_amd64.deb",
+ "name": "p11-kit",
+ "sha256": "d816fde1ff781f00facca43d37684085c7286d7056ad990e40d3d162e6f511d6",
+ "source": "p11-kit",
+ "version": "0.26.2-2+pasturestack.1"
+ },
+ {
+ "file": "p11-kit-modules_0.26.2-2+pasturestack.1_amd64.deb",
+ "name": "p11-kit-modules",
+ "sha256": "1429e81a3a7ba4b148e0966af4abc441dcfd2179e41efd8fb1c8ad7016e94f4f",
+ "source": "p11-kit",
+ "version": "0.26.2-2+pasturestack.1"
+ }
+ ],
+ "runConclusion": "success",
+ "runId": 40121338859,
+ "runtimePackages": [
+ "libp11-kit0",
+ "p11-kit",
+ "p11-kit-modules"
+ ],
+ "sourceCommit": "2a47f3cd99b6afc47b54ee6d8837b834e95c50f2",
+ "sourceFiles": {
+ "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2",
+ "server/security/ubuntu-backports/p11-kit/build-p11-kit-backport.py": "2c4e2e7357ebebc12c2698a4b626393cecd9b46ba84b3698934a4a912e75a96e",
+ "server/security/ubuntu-backports/p11-kit/p11-kit.lock.json": "636bfbe8d9730e1838207edd9e3a6108f373d601c5b13591888913c8a042d046",
+ "server/security/ubuntu-backports/p11-kit/test_p11_kit_backport.py": "16505ce478e2134dafc78e722ee40786d5390a99f60945abb684b2a9010deb86",
+ "server/security/ubuntu-backports/source_helpers.py": "d2b3f310f8ed99a50a696fe14094b7117ec5d7ed576e6080d5540c0e82891486"
+ },
+ "status": "qualified",
+ "vexScope": "Exact official recursion-limit security patches in qualified amd64 packages. CVE-2026-18938 is officially a 32-bit-only trigger; amd64 owning-suite PASS is not 32-bit execution coverage.",
+ "zipSha256": "2fab74734ae7e24d76b57a11d783fb0136399c05e9651f86019c68b8fa633f21"
+ },
+ "rust-coreutils": {
+ "artifactId": 12571737310,
+ "artifactName": "ubuntu-native-rust-coreutils-07e4a7afa827cb45514a72aba12f03b1d30d121d-40120637646",
+ "ciClosure": {
+ ".github/workflows/ubuntu-backport-native.yml": {
+ "currentSha256": "329ad26ee9f4b22f6fae37b8d680c9bcf6e9dc65e22a016d4225b1dcba606b79",
+ "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."
+ },
+ "server/security/ubuntu-backports/Dockerfile.native-ci": {
+ "currentSha256": "ba6f233913a679b53f0234423a7c281e31c36b8305fde354b4ab177e05890f0b",
+ "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."
+ }
+ },
+ "copyright": {
+ "file": "copyright",
+ "sha256": "f1ea5d8870ff83301cc4c129ecd5e0e3c998bd566a9b0036df899e84d9336bd6"
+ },
+ "evidence": {
+ "builder-packages.tsv": "6cdb72f354d275dcb8e34274fd324329a6734320c01b519c8c6898c62e27825d",
+ "copyright": "f1ea5d8870ff83301cc4c129ecd5e0e3c998bd566a9b0036df899e84d9336bd6",
+ "provenance.json": "f06fd1b74848aeb0ef9d23f38c3e0700e167d935e7c7fcf7879c8e3561c23391",
+ "rust-coreutils.lock.json": "e52adb460ec7b01accc2f3f9cb3dccc8923e2619d5ced41aae1722494c81bd41",
+ "rust-coreutils_0.10.0-1ubuntu2~26.04.1+pasturestack.1_amd64.buildinfo": "d87448f36eeb16399c10b4c16e0e46cb75bb6fa26274ff6c5e957da5c88e29bd",
+ "rust-coreutils_0.10.0-1ubuntu2~26.04.1+pasturestack.1_amd64.changes": "4fcc000e8f8629209b6e1e925548a6448dd9c2a836eb03590067116edc4f860e",
+ "strict-tests.log": "b4eba1bcfa7299a6fb2cbf76e19424cb1f19555779b71f14823a29b8bc43a3a7"
+ },
+ "fixedVulnerabilities": [
+ "CVE-2026-35370",
+ "CVE-2026-35371",
+ "CVE-2026-35341",
+ "CVE-2026-35344",
+ "CVE-2026-35345",
+ "CVE-2026-35348",
+ "CVE-2026-35350",
+ "CVE-2026-35351",
+ "CVE-2026-35352",
+ "CVE-2026-35354",
+ "CVE-2026-35357",
+ "CVE-2026-35359",
+ "CVE-2026-35360",
+ "CVE-2026-35363",
+ "CVE-2026-35364",
+ "CVE-2026-35367",
+ "CVE-2026-35368",
+ "CVE-2026-35373",
+ "CVE-2026-35374",
+ "CVE-2026-35377"
+ ],
+ "jobId": 120951205262,
+ "lockSha256": "e52adb460ec7b01accc2f3f9cb3dccc8923e2619d5ced41aae1722494c81bd41",
+ "packages": [
+ {
+ "file": "rust-coreutils_0.10.0-1ubuntu2~26.04.1+pasturestack.1_amd64.deb",
+ "name": "rust-coreutils",
+ "sha256": "f90cdece3ffa01acc9153e3741725febb696e02db8ccf6c0d0c4f98ff93ec609",
+ "source": "rust-coreutils",
+ "version": "0.10.0-1ubuntu2~26.04.1+pasturestack.1"
+ }
+ ],
+ "runConclusion": "success",
+ "runId": 40120637646,
+ "runtimePackages": [
+ "rust-coreutils"
+ ],
+ "sourceAudit": {
+ "originalReceiptSha256": "73189028622c0a0fc074067b4c5b8521094921531f95cf3e68073baaebd2fbbd",
+ "path": "rust-coreutils-original18-source-closure.json",
+ "sha256": "27bc317cfac11edd24b2c8beebf5094d84c8e312d4bf0cf4e61f05a300daacc8"
+ },
+ "sourceCommit": "07e4a7afa827cb45514a72aba12f03b1d30d121d",
+ "sourceFiles": {
+ "server/security/ubuntu-backports/rust-coreutils/build-rust-coreutils-backport.py": "c94021aa839dfe37ad69978cc8d9d4e27308453c83a318d07ada3d4b1f42a6f0",
+ "server/security/ubuntu-backports/rust-coreutils/native-tests.mk": "8474160f550e8475fe743ce2fe737bdb60d097aaabfb7859fd4a2144c216215d",
+ "server/security/ubuntu-backports/rust-coreutils/rust-coreutils.lock.json": "e52adb460ec7b01accc2f3f9cb3dccc8923e2619d5ced41aae1722494c81bd41",
+ "server/security/ubuntu-backports/rust-coreutils/test_rust_coreutils_backport.py": "7ab36b954fcb2248489d69ad1ec08ba7aa578027bba3ccb8b4cb40dae6e65702"
+ },
+ "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"
+ }
+ },
+ "pcre2": {
+ "copyrightEvidence": {
+ "creationHunkLines": 122,
+ "path": "debian/copyright",
+ "source": {
+ "name": "pcre2_10.46-1build1.diff.gz",
+ "sha256": "3534c69dec410207e74bf9fd6ed334ad5827fb6de1d0715175b7a14257c5bc72",
+ "size": 8804,
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/pcre2/pcre2_10.46-1build1.diff.gz"
+ }
+ },
+ "copyrightSha256": "e915cde4251f96ec6af107ecd2e194440a4e69440893b0c982802f8a1e4ae40a",
+ "fixedVulnerabilities": [
+ "CVE-2026-103111",
+ "CVE-2026-86145",
+ "CVE-2026-89156",
+ "CVE-2026-89157",
+ "CVE-2026-89158",
+ "CVE-2026-89160",
+ "CVE-2026-89161",
+ "CVE-2026-89162"
+ ],
+ "kind": "fresh-production-target",
+ "lockSha256": "c9bddd7006761549e30843a83c097fa4019b1a58262755a1c2dc23e10ba0aeb5",
+ "runtimePackages": [
+ "libpcre2-8-0",
+ "libpcre2-posix3"
+ ],
+ "sourceCommit": "9abef4712901981e5917c41356bb261974fdba47",
+ "sourceFiles": {
+ "server/Dockerfile.web-compose-release": "17de14127a2414ecc8b410f483b46582aa252964bcc849a83678665cdefda1bb",
+ "server/scripts/download-ubuntu-security-updates.sh": "4284321d2b73802046d706f5fbd683b88ffc351187be8bc93d20bd3cf97b7216",
+ "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2",
+ "server/security/ubuntu-backports/pcre2.lock.json": "c9bddd7006761549e30843a83c097fa4019b1a58262755a1c2dc23e10ba0aeb5",
+ "server/security/ubuntu-security-packages.lock": "8e43275a0b7b13a2a5d825eea2b15c68ec75a27f50dd6c0f497aadba164c6704"
+ },
+ "sourceTag": "v1.6.551",
+ "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."
+ },
+ "platform": {
+ "architecture": "amd64",
+ "distribution": "ubuntu",
+ "release": "26.04"
+ },
+ "repository": "PastureStack/server",
+ "schemaVersion": 1,
+ "trackedScopes": {
+ "consumerStatus": "NOT_PRODUCT_REMEDIATION_PROOF",
+ "schemaVersion": 1,
+ "statements": [
+ {
+ "reason": "All 18 exact Ubuntu orig + enabled quilt security controls were reviewed with no effective revert, and are bound to the qualified native artifact, source lock, provenance and package SHA. This is component-source evidence; consumer/product remediation remains a separate gate.",
+ "scope": "rust-coreutils prior fixes",
+ "sourceAudit": {
+ "originalReceiptSha256": "73189028622c0a0fc074067b4c5b8521094921531f95cf3e68073baaebd2fbbd",
+ "path": "rust-coreutils-original18-source-closure.json",
+ "sha256": "6057ffc353b5cdd86ba887354d2f1ee1f01afd11a4ba3bbca3f16b2b4600b3a5"
+ },
+ "status": "qualified_component_source_closed",
+ "vulnerabilities": [
+ "CVE-2026-35341",
+ "CVE-2026-35344",
+ "CVE-2026-35345",
+ "CVE-2026-35348",
+ "CVE-2026-35350",
+ "CVE-2026-35351",
+ "CVE-2026-35352",
+ "CVE-2026-35354",
+ "CVE-2026-35357",
+ "CVE-2026-35359",
+ "CVE-2026-35360",
+ "CVE-2026-35363",
+ "CVE-2026-35364",
+ "CVE-2026-35367",
+ "CVE-2026-35368",
+ "CVE-2026-35373",
+ "CVE-2026-35374",
+ "CVE-2026-35377"
+ ]
+ },
+ {
+ "reason": "No qualified tar package is part of this 12-runtime-package bundle; CVE-2026-18508 requires a separate structural API assessment.",
+ "scope": "tar",
+ "status": "not_in_bundle",
+ "vulnerabilities": [
+ "CVE-2026-18477",
+ "CVE-2026-18508"
+ ]
+ },
+ {
+ "reason": "jq uses system Oniguruma. No claim about Ruby/Onigmo, other runtime dependency packages or build-host scanners is made.",
+ "scope": "Ruby/Onigmo build-host dependency gap",
+ "status": "tracked_separately"
+ },
+ {
+ "reason": "A scratch bundle scan with zero OS findings does not establish that these deb payloads or eventual consumer root filesystems are free of vulnerabilities.",
+ "scope": "consumer images",
+ "status": "pending_install_rootfs_scan_and_product_acceptance"
+ }
+ ]
+ },
+ "vexTimestamp": "2026-10-25T00:00:00Z"
+}
diff --git a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py
new file mode 100644
index 0000000000..5dd81fe1de
--- /dev/null
+++ b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py
@@ -0,0 +1,323 @@
+#!/usr/bin/env python3
+"""Small negative/positive checks for the bundle trust boundaries; no builds."""
+import copy
+import importlib.util
+import io
+import json
+from pathlib import Path
+import stat
+import struct
+import tarfile
+import tempfile
+from types import SimpleNamespace
+import unittest
+from unittest.mock import patch
+import urllib.error
+import zipfile
+
+HERE = Path(__file__).resolve().parent
+SPEC = importlib.util.spec_from_file_location('qualifier', HERE / 'qualify-native-packages.py')
+Q = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(Q)
+
+
+def fixture_api(item, family='jq'):
+ return {'run': {'id': item['runId'], 'status': 'completed', 'head_sha': item['sourceCommit'],
+ 'conclusion': item['runConclusion'], 'path': '.github/workflows/ubuntu-backport-native.yml'},
+ 'job': {'id': item['jobId'], 'run_id': item['runId'], 'head_sha': item['sourceCommit'],
+ 'name': 'Native (' + family + ')', 'status': 'completed', 'conclusion': 'success'},
+ 'artifact': {'id': item['artifactId'], 'expired': False, 'name': item['artifactName'],
+ 'digest': 'sha256:' + item['zipSha256'],
+ 'workflow_run': {'id': item['runId'], 'head_sha': item['sourceCommit']}}}
+
+
+class Boundaries(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory(prefix='bundle-unit-')
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name)
+ self.lock = Q.load_lock()
+
+ def make_zip(self, names):
+ archive = self.root / 'native.zip'
+ with zipfile.ZipFile(archive, 'w') as target:
+ for name, content in names:
+ if isinstance(name, str):
+ info = zipfile.ZipInfo('entry'); info.filename = info.orig_filename = name
+ target.writestr(info, content)
+ else: target.writestr(name, content)
+ return archive
+
+ def test_pending_glibc_is_not_publishable(self):
+ lock = copy.deepcopy(self.lock)
+ lock['nativeInputs']['glibc'].update(status='pending', artifactId=None, zipSha256=None)
+ file = self.root / 'lock.json'; file.write_text(json.dumps(lock))
+ with patch.object(Q, 'LOCK_FILE', file):
+ self.assertEqual(Q.load_lock()['nativeInputs']['glibc']['status'], 'pending')
+ with self.assertRaisesRegex(ValueError, 'pending'):
+ 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()})
+ 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))
+ self.assertTrue(all(call.args[0][:3] == ['git', 'cat-file', '-e'] for call in run.call_args_list))
+ replies = [SimpleNamespace(returncode=1), SimpleNamespace(returncode=0), SimpleNamespace(returncode=0)]
+ replies += [SimpleNamespace(returncode=0) for _ in commits[1:]]
+ with patch.object(Q.subprocess, 'run', side_effect=replies) as run:
+ Q.ensure_native_sources(self.lock)
+ 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_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')]:
+ lock = copy.deepcopy(self.lock); mutate(lock)
+ file = self.root / 'lock.json'; file.write_text(json.dumps(lock))
+ with patch.object(Q, 'LOCK_FILE', file), self.assertRaises(ValueError): Q.load_lock()
+
+ def test_zip_requires_external_pin(self):
+ archive = self.make_zip([('export/result.json', b'{}')])
+ with self.assertRaisesRegex(ValueError, 'reviewed artifact pin'): Q.read_zip(archive, '0' * 64)
+ self.assertEqual(Q.read_zip(archive, Q.file_sha(archive)), {'export/result.json': b'{}'})
+
+ def test_zip_rejects_escaping_alias_and_diagnostic_paths(self):
+ for name in ['../escape', '/absolute', 'C:/escape', 'a\\b', 'a/./b', 'a//b', '.', 'line\nbreak',
+ 'export/diagnostics/unqualified-packages/libc6.deb']:
+ with self.subTest(name=name):
+ archive = self.make_zip([(name, b'x')])
+ with self.assertRaises(ValueError): Q.read_zip(archive, Q.file_sha(archive))
+
+ def test_zip_rejects_duplicate_case_and_links(self):
+ archive = self.make_zip([('A', b'x'), ('a', b'y')])
+ with self.assertRaises(ValueError): Q.read_zip(archive, Q.file_sha(archive))
+ link = zipfile.ZipInfo('link'); link.create_system = 3
+ link.external_attr = (stat.S_IFLNK | 0o777) << 16
+ archive = self.make_zip([(link, b'/etc/passwd')])
+ with self.assertRaises(ValueError): Q.read_zip(archive, Q.file_sha(archive))
+
+ def test_selected_job_success_preserves_actual_matrix_failure(self):
+ item = self.lock['nativeInputs']['jq']; api = fixture_api(item)
+ self.assertEqual(api['run']['conclusion'], 'failure')
+ Q.verify_api('jq', item, **api)
+ for section, key, value in [('job', 'conclusion', 'failure'), ('job', 'run_id', 1),
+ ('artifact', 'expired', True), ('run', 'head_sha', 'a' * 40),
+ ('run', 'conclusion', 'success')]:
+ changed = copy.deepcopy(api); changed[section][key] = value
+ with self.subTest(section=section, key=key), self.assertRaises(ValueError):
+ Q.verify_api('jq', item, **changed)
+
+ def test_artifact_api_cannot_rebind_same_named_bytes(self):
+ item = self.lock['nativeInputs']['jq']; api = fixture_api(item)
+ api['artifact']['workflow_run']['id'] += 1
+ with self.assertRaises(ValueError): Q.verify_api('jq', item, **api)
+
+ def test_native_result_requires_all_three_numeric_zero_exits(self):
+ original = self.lock['nativeInputs']['jq']
+ for key, value in [('exitCode', 1), ('helperExitCode', True), ('logExitCode', 1)]:
+ result = {'module': 'jq', 'sourceSha': original['sourceCommit'], 'status': 'passed',
+ 'exitCode': 0, 'helperExitCode': 0, 'logExitCode': 0,
+ 'sourceLockSha256': original['lockSha256']}; result[key] = value
+ archive = self.make_zip([('source-sha.txt', original['sourceCommit']),
+ ('export/result.json', json.dumps(result))])
+ item = copy.deepcopy(original); item['zipSha256'] = Q.file_sha(archive); item['sourceFiles'] = {}
+ with self.subTest(key=key), self.assertRaisesRegex(ValueError, 'exit/lock'):
+ Q.validate_native('jq', item, archive, fixture_api(item), self.root, self.root / 'tests')
+
+ def test_pcre_requires_each_actual_suite_and_global_pass(self):
+ names = ['pcre2_jit_test', 'RunTest', 'RunGrepTest']
+ good = b':test-result: PASS\n:global-test-result: PASS\n'
+ files = {name + '.trs': good for name in names}
+ files.update({name + '.log': b'actual native output' for name in names})
+ provenance = {'tests': [{'test': n, 'sha256': Q.sha(good), 'status': 'PASS'} for n in names]}
+ Q.check_owning_tests('pcre2', files, provenance, self.root / 'good')
+ for number, wrong in enumerate([b':test-result: PASS\n:global-test-result: FAIL\n',
+ good + b':test-result: FAIL\n', b':test-result: SKIP\n']):
+ files[names[0] + '.trs'] = wrong; provenance['tests'][0]['sha256'] = Q.sha(wrong)
+ with self.assertRaises(ValueError):
+ Q.check_owning_tests('pcre2', files, provenance, self.root / ('bad' + str(number)))
+
+ def tar_payload(self, entries):
+ stream = io.BytesIO()
+ with tarfile.open(fileobj=stream, mode='w') as archive:
+ for name, kind, content in entries:
+ member = tarfile.TarInfo(name); member.mode = 0o755; member.uid = member.gid = 0
+ member.type = kind
+ if kind == tarfile.REGTYPE:
+ member.size = len(content); archive.addfile(member, io.BytesIO(content))
+ else:
+ member.linkname = content; archive.addfile(member)
+ return stream.getvalue()
+
+ def payload(self, entries, source='rust-coreutils'):
+ data = self.tar_payload(entries)
+ fields = {'Package': b'rust-coreutils', 'Source': source.encode(), 'Version': b'1.0', 'Architecture': b'amd64'}
+ def command(args):
+ if args[1] == '--field': return fields[args[-1]]
+ if args[1] == '--fsys-tarfile': return data
+ raise AssertionError(args)
+ with patch.object(Q, 'command', command):
+ return Q.package_payload(self.root / 'native.deb',
+ {'name': 'rust-coreutils', 'source': 'rust-coreutils', 'version': '1.0'}, self.root)
+
+ def test_deb_payload_resolves_hardlinks_without_extracting(self):
+ payload = self.payload([('./usr/bin/coreutils', tarfile.REGTYPE, b'payload'),
+ ('./usr/bin/id', tarfile.LNKTYPE, './usr/bin/coreutils'),
+ ('./usr/bin/realpath', tarfile.SYMTYPE, '/usr/bin/coreutils')])
+ files = {p['path']: p for p in payload['files']}
+ self.assertEqual(files['usr/bin/id']['sha256'], Q.sha(b'payload'))
+ self.assertEqual(files['usr/bin/realpath']['target'], '/usr/bin/coreutils')
+ self.assertFalse((self.root / 'usr').exists())
+
+ def test_deb_rejects_traversal_special_cycle_and_symlink_parent(self):
+ for entries in [[('../escape', tarfile.REGTYPE, b'x')],
+ [('device', tarfile.FIFOTYPE, '')],
+ [('id', tarfile.LNKTYPE, 'other'), ('other', tarfile.LNKTYPE, 'id')],
+ [('usr', tarfile.SYMTYPE, '/etc'), ('usr/passwd', tarfile.REGTYPE, b'x')]]:
+ with self.subTest(entries=entries), self.assertRaises(ValueError): self.payload(entries)
+
+ def test_deb_rejects_wrong_source(self):
+ with self.assertRaisesRegex(ValueError, 'control identity'):
+ self.payload([('file', tarfile.REGTYPE, b'x')], source='other')
+
+ def test_elf_metadata_uses_dynamic_external_symbols_and_versions(self):
+ elf = bytearray(64); elf[:6] = b'\x7fELF\x02\x01'; struct.pack_into(' 0,
+ 'SOURCE_DATE_EPOCH must be a positive integer')
+ upstream = lock['upstream']
+ require(upstream['repository'] == 'https://sourceware.org/git/glibc.git'
+ and upstream['backportBranch'] == 'release/2.43/master'
+ and upstream['fixCommit'] == FIX and upstream['regressionCommit'] == REGRESSION,
+ 'Only the official 2.43 fix and its regression backport are supported')
+ sources, patches = lock['sourceFiles'], lock['patches']
+ require(len(sources) == 4 and {i['name'] for i in sources} == set(SOURCE_PINS),
+ 'Exactly the original Ubuntu DSC/tarballs/signature are required')
+ require([i['commit'] for i in patches] == [FIX, REGRESSION], 'Unexpected patch set/order/branch')
+ for item in sources + patches:
+ name = item['name']
+ require(Path(name).name == name and '/' not in name and '\\' not in name, 'Unsafe input filename')
+ if item in sources:
+ pin, size = SOURCE_PINS[name]
+ url = 'https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/' + name
+ else:
+ pin, size = PATCH_PINS[item['commit']]
+ require(name == item['commit'] + '.sourceware.patch', 'Unexpected patch filename')
+ url = 'https://sourceware.org/git/?p=glibc.git;a=patch;h=' + item['commit']
+ require(item['sha256'] == pin and type(item['size']) is int and item['size'] == size,
+ 'Input differs from approved source SHA-256/length')
+ require(item['url'] == url, 'Unexpected official source URL')
+ require('First-party' in lock['identity'] and 'not issued by Canonical' in lock['identity'],
+ 'Custom output must not claim Canonical provenance')
+ require(lock['packagingMetadataPolicy']['originalControlSha256'] == CONTROL_ORIGINAL_SHA256
+ and lock['packagingMetadataPolicy']['generatedControlSha256'] == CONTROL_GENERATED_SHA256
+ and lock['packagingMetadataPolicy']['approvedChangedFields'] ==
+ ['Maintainer', 'XSBC-Original-Maintainer'], 'Unexpected generated control metadata policy')
+ return lock
+
+
+def download(item, directory, cache):
+ """Reuse common HTTPS/hash checks; glibc's 20MiB input needs its own pinned-size bound."""
+ destination = directory / item['name']
+ cached = cache / item['name'] if cache else None
+ if cached and cached.exists():
+ COMMON.verify_input(item, cached)
+ shutil.copyfile(cached, destination)
+ else:
+ opener = urllib.request.build_opener(COMMON.HttpsRedirect())
+ with opener.open(item['url'], timeout=60) as response, destination.open('xb') as output:
+ require(response.url.startswith('https://'), 'Refusing non-HTTPS input')
+ size = 0
+ while True:
+ chunk = response.read(65536)
+ if not chunk:
+ break
+ size += len(chunk)
+ require(size <= item['size'], 'Input exceeds its pinned size')
+ output.write(chunk)
+ COMMON.verify_input(item, destination)
+ if cached:
+ shutil.copyfile(destination, cached)
+ COMMON.verify_input(item, destination)
+ return destination
+
+
+def verify_source_control(lock, directory):
+ text = (directory / 'glibc_2.43-2ubuntu2.4.dsc').read_text(encoding='utf-8')
+ require(all(re.search('^' + re.escape(line) + '$', text, re.M) for line in
+ ['Source: glibc', 'Version: 2.43-2ubuntu2.4', 'Format: 3.0 (quilt)']),
+ 'Unexpected Ubuntu source control')
+ block = re.search(r'^Checksums-Sha256:\n((?: .+\n)+)', text, re.M)
+ require(block is not None, 'DSC lacks source checksums')
+ rows = re.findall(r'^ ([0-9a-f]{64}) (\d+) (\S+)$', block[1], re.M)
+ expected = {i['name']: (i['sha256'], i['size']) for i in lock['sourceFiles'] if not i['name'].endswith('.dsc')}
+ require(len(rows) == 3 and {name: (sha, int(size)) for sha, size, name in rows} == expected,
+ 'DSC does not bind exactly the approved original tarball, signature and packaging')
+
+
+def verify_patch(item, file):
+ COMMON.verify_input(item, file)
+ text = file.read_text(encoding='utf-8')
+ require(text.startswith('From ' + item['commit'] + ' '), 'Patch commit identity mismatch')
+ expected = ['libio/fileops.c'] if item['commit'] == FIX else ['libio/Makefile', 'libio/tst-fopen-ccs-empty.c']
+ paths = re.findall(r'^diff --git a/(\S+) b/(\S+)$', text, re.M)
+ require(paths == [(p, p) for p in expected], 'Patch must stay within its approved upstream paths')
+ require(not re.search(r'^(?:old mode|new mode|rename from|rename to|GIT binary patch)', text, re.M),
+ 'Unexpected patch metadata')
+
+
+def validate_inputs(lock, directory):
+ for item in lock['sourceFiles'] + lock['patches']:
+ COMMON.verify_input(item, directory / item['name'])
+ verify_source_control(lock, directory)
+ for item in lock['patches']:
+ verify_patch(item, directory / item['name'])
+
+
+def build_environment(lock, jobs):
+ # Package rules supply their existing flags/CET/symbol policy. Inherited
+ # compiler/profile/test overrides cannot weaken those settings.
+ env = COMMON.build_environment(lock, jobs)
+ env['DEB_BUILD_PROFILES'] = 'nobiarch noudeb'
+ return env
+
+
+def source_closure(source):
+ text = (source / 'libio/fileops.c').read_text(encoding='utf-8')
+ require(NEW_CALL in text and OLD_CALL not in text, 'Official source closure is absent')
+ require('tst-fopen-ccs-empty' in (source / 'libio/Makefile').read_text(encoding='utf-8')
+ and (source / 'libio/tst-fopen-ccs-empty.c').is_file(), 'Official regression is not registered')
+ return {p: digest(source / p) for p in
+ ['libio/fileops.c', 'libio/Makefile', 'libio/tst-fopen-ccs-empty.c']}
+
+
+def parse_test_summary(text):
+ entries, headers, sections = [], 0, []
+ for line in text.splitlines():
+ if not line.strip():
+ continue
+ if line.strip() == '=== glibc tests ===':
+ require(not entries and not sections and headers == 0, 'Ambiguous native test header')
+ headers += 1
+ continue
+ section = re.fullmatch(r'Running (\.|[a-z0-9][a-z0-9_-]*) \.\.\.', line)
+ if section:
+ require(headers == 1 and section[1] not in sections, 'Ambiguous native test section')
+ sections.append(section[1])
+ continue
+ result = re.fullmatch(r'(PASS|FAIL|XFAIL|XPASS|UNSUPPORTED|UNRESOLVED|ERROR): (\S+)', line)
+ require(result is not None and headers == 1 and sections,
+ 'Unknown native tests.sum line: ' + line)
+ entries.append(result.groups())
+ require(headers == 1 and sections and entries
+ and len({name for _, name in entries}) == len(entries), 'Ambiguous native test results')
+ return entries
+
+
+def verify_packaging_inputs(source, expected):
+ changed = [name for name, sha in expected.items()
+ if not (source / name).is_file() or digest(source / name) != sha]
+ require(not changed, 'Build changed approved Ubuntu packaging inputs: ' + ', '.join(changed))
+
+
+def prepare_generated_control(source, environment, log):
+ before = COMMON.packaging_snapshot(source)
+ require(before.get('debian/control') == CONTROL_ORIGINAL_SHA256,
+ 'Fresh original Ubuntu control metadata is required')
+ command = ['make', '--no-print-directory', '-f', 'debian/rules', 'debian/control']
+ run(command, source, environment, log)
+ after = COMMON.packaging_snapshot(source)
+ changed = sorted(name for name in before.keys() | after.keys() if before.get(name) != after.get(name))
+ require(changed == ['debian/control'], 'Unexpected generated packaging changes: ' + ', '.join(changed))
+ require(after['debian/control'] == CONTROL_GENERATED_SHA256,
+ 'Generated control differs from the approved original-template output')
+ return {'command': command, 'path': 'debian/control',
+ 'originalSha256': before['debian/control'], 'generatedSha256': after['debian/control'],
+ 'inputSha256': {name: sha for name, sha in before.items()
+ if name.startswith('debian/control.in/') or
+ name in ['debian/rules', 'debian/rules.d/control.mk']},
+ 'changedFields': {
+ 'Maintainer': {'from': 'Ubuntu Developers ',
+ 'to': 'GNU Libc Maintainers '},
+ 'XSBC-Original-Maintainer': {'from': 'GNU Libc Maintainers ',
+ 'to': None}},
+ 'semantics': 'Preserve Ubuntu source packaging and original control templates; first-party metadata follows the original make rules and is not a Canonical publication.'}
+
+
+def test_receipt(source):
+ report = source / 'build-tree/amd64-libc/tests.sum'
+ require(report.is_file(), 'Missing full native tests.sum')
+ entries = parse_test_summary(report.read_text(encoding='utf-8'))
+ states = dict((name, state) for state, name in entries)
+ counts = collections.Counter(state for state, _ in entries)
+ require(not any(counts[s] for s in ['FAIL', 'ERROR', 'UNRESOLVED']), 'Unexpected native test failure')
+ require(all(states.get(name) == 'PASS' for name in CONTROLS), 'CVE regression/valid charset controls did not PASS')
+ for component in ['libio', 'stdio-common', 'iconv', 'wcsmbs', 'malloc', 'elf', 'nptl', 'nss']:
+ require(any(state == 'PASS' and name.startswith(component + '/') for state, name in entries),
+ 'Native component suite did not execute: ' + component)
+ require((source / 'stamp-dir/check_libc_passed').is_file()
+ and not (source / 'stamp-dir/check_libc_failed').exists(), 'Ubuntu native test gate did not pass')
+ require(not (source / 'build-tree/amd64-i386').exists()
+ and not (source / 'build-tree/amd64-x32').exists(), 'Unexpected extra ABI build')
+ return {'report': 'build-tree/amd64-libc/tests.sum', 'sha256': digest(report), 'counts': dict(counts),
+ 'controls': {name: states[name] for name in CONTROLS}, 'ubuntuGate': 'check_libc_passed',
+ 'semantics': 'XFAIL/UNSUPPORTED/XPASS are retained verbatim under unchanged Ubuntu rules; they are not zero failures or universal coverage.'}
+
+
+def verify_debs(lock, work, source, environment, log, dev_packages):
+ selected = []
+ extracted = work / 'verify-runtime'
+ extracted.mkdir()
+ for package in PACKAGES + dev_packages:
+ matches = list(work.glob(package + '_' + lock['customVersion'] + '_amd64.deb'))
+ require(len(matches) == 1, 'Missing/ambiguous selected deb: ' + package)
+ deb = matches[0]
+ for field, value in [('Package', package), ('Version', lock['customVersion']), ('Architecture', 'amd64')]:
+ require(COMMON.deb_field(deb, field, work, environment) == value, 'Unexpected deb metadata: ' + field)
+ require(COMMON.deb_field(deb, 'Source', work, environment) in
+ ['glibc', 'glibc (' + lock['customVersion'] + ')'], 'Unexpected deb Source identity')
+ run(['dpkg-deb', '--extract', str(deb), str(extracted)], work, environment, log)
+ selected.append({'package': package, 'version': lock['customVersion'], 'architecture': 'amd64',
+ 'file': deb.name, 'sha256': digest(deb),
+ 'role': 'runtime' if package in PACKAGES else 'explicit-consumer-dev-compatibility'})
+ libc = extracted / 'usr/lib/x86_64-linux-gnu/libc.so.6'
+ require(libc.is_file() and libc.resolve().is_relative_to(extracted.resolve()), 'Candidate libc is absent or escapes extraction')
+ dynamic = capture(['readelf', '--dynamic', str(libc)], work, environment)
+ headers = capture(['readelf', '--program-headers', str(libc)], work, environment)
+ elf = capture(['readelf', '--file-header', str(libc)], work, environment)
+ require('ELF64' in elf and 'X86-64' in elf.upper(), 'Candidate libc is not native amd64 ELF64')
+ require('[libc.so.6]' in dynamic and 'GNU_RELRO' in headers
+ and ('BIND_NOW' in dynamic or re.search(r'FLAGS_1.*NOW', dynamic)), 'libc SONAME/hardening changed')
+ symbols = capture(['nm', '--dynamic', '--defined-only', str(libc)], work, environment)
+ require(all(re.search(r'\b' + name + r'@@?GLIBC_[0-9.]+$', symbols, re.M)
+ for name in ['fopen', 'fopen64', 'malloc', '__libc_start_main']), 'Required versioned libc ABI missing')
+ require((extracted / 'usr/sbin/ldconfig').is_file(), 'Selected libc-bin lacks ldconfig')
+ require(list((extracted / 'usr/lib/x86_64-linux-gnu/gconv').glob('*.so')), 'Selected gconv-extra has no modules')
+ return selected
+
+
+def build(args, lock):
+ os_release = dict(re.findall(r'^(\w+)=["\']?([^"\'\n]+)', Path('/etc/os-release').read_text(encoding='utf-8'), re.M))
+ require(os_release.get('ID') == 'ubuntu' and os_release.get('VERSION_ID') == '26.04', 'Use an isolated Ubuntu 26.04 builder')
+ environment = build_environment(lock, args.jobs)
+ for command in ['dpkg', 'dpkg-source', 'dpkg-buildpackage', 'dpkg-deb', 'dpkg-buildflags', 'dpkg-checkbuilddeps',
+ 'dpkg-query', 'quilt', 'readelf', 'nm', 'fakeroot', 'make']:
+ require(shutil.which(command, path=environment['PATH']), 'Missing builder dependency: ' + command)
+ require(capture(['dpkg', '--print-architecture'], HERE, environment) == 'amd64', 'Use a native amd64 builder')
+ output = args.output_dir.resolve()
+ require(not output.exists(), 'Output directory must be new')
+ cache = args.cache_dir.resolve() if args.cache_dir else None
+ COMMON.require_separate_paths(output, args.work_dir.resolve() if args.work_dir else None, cache)
+ work = args.work_dir.resolve() if args.work_dir else Path(tempfile.mkdtemp(prefix='glibc-ubuntu-backport-'))
+ if args.work_dir:
+ work.mkdir(parents=True, exist_ok=False)
+ print('GLIBC_BACKPORT_WORK_DIR=' + str(work), flush=True)
+ inputs = work / 'inputs'
+ inputs.mkdir()
+ if cache:
+ cache.mkdir(parents=True, exist_ok=True)
+ for item in lock['sourceFiles'] + lock['patches']:
+ download(item, inputs, cache)
+ validate_inputs(lock, inputs)
+ log = work / 'build.log'
+ source = work / 'glibc-source'
+ run(['dpkg-source', '--extract', str(inputs / 'glibc_2.43-2ubuntu2.4.dsc'), str(source)], work, environment, log)
+ packaging = COMMON.packaging_snapshot(source)
+ original_series = (source / 'debian/patches/series').read_text(encoding='utf-8')
+ require(OLD_CALL in (source / 'libio/fileops.c').read_text(encoding='utf-8'), 'Original source boundary changed')
+ environment['QUILT_PATCHES'] = 'debian/patches'
+ for item in lock['patches']:
+ run(['quilt', 'import', '-P', item['name'], str(inputs / item['name'])], source, environment, log)
+ run(['quilt', 'push', '--fuzz=0'], source, environment, log)
+ require(all(digest(source / name) == sha for name, sha in packaging.items()
+ if name != 'debian/patches/series'), 'Official backports changed Ubuntu packaging rules')
+ series = (source / 'debian/patches/series').read_text(encoding='utf-8')
+ require(series.startswith(original_series) and series[len(original_series):].split()
+ == [item['name'] for item in lock['patches']], 'Existing quilt series changed or extra patches added')
+ source_proof = source_closure(source)
+ changelog = source / 'debian/changelog'
+ date = email.utils.format_datetime(datetime.datetime.fromtimestamp(lock['sourceDateEpoch'], datetime.timezone.utc))
+ changelog.write_text('glibc (' + lock['customVersion'] + ') resolute; urgency=high\n\n'
+ ' * First-party PastureStack rebuild; not issued by Canonical.\n'
+ ' * Apply official glibc 2.43 CVE-2026-18374 fix and regression test.\n\n'
+ ' -- PastureStack maintainers ' + date + '\n\n'
+ + changelog.read_text(encoding='utf-8'), encoding='utf-8')
+ generated_control = prepare_generated_control(source, environment, log)
+ approved_packaging = COMMON.packaging_snapshot(source)
+ # -G emits custom source provenance plus arch-specific binaries. Original
+ # tarballs stay beside the new DSC; no distro-independent docs are built.
+ for item in lock['sourceFiles']:
+ if '.orig.tar.' in item['name']:
+ shutil.copyfile(inputs / item['name'], work / item['name'])
+ run(['dpkg-checkbuilddeps', '-B', '-P' + ','.join(lock['buildProfiles'])], source, environment, log)
+ command = ['dpkg-buildpackage', '-G', '-us', '-uc', '-P' + ','.join(lock['buildProfiles']), COMMON.job_option(args.jobs)]
+ run(command, source, environment, log)
+ verify_packaging_inputs(source, approved_packaging)
+ require(source_closure(source) == source_proof, 'Built source no longer matches the approved fix')
+ tests = test_receipt(source)
+ # Return package names from the fixed catalog, never CLI-derived path tokens.
+ dev_exports = [package for package in DEV_PACKAGES if package in args.export_dev_package]
+ selected = verify_debs(lock, work, source, environment, log, dev_exports)
+ output.mkdir(parents=True)
+ for item in selected:
+ shutil.copyfile(work / item['file'], output / item['file'])
+ for pattern in ['glibc_*.buildinfo', 'glibc_*.changes', 'glibc_' + lock['customVersion'] + '.dsc',
+ 'glibc_' + lock['customVersion'] + '.debian.tar.*']:
+ files = list(work.glob(pattern))
+ require(len(files) == 1, 'Missing/ambiguous custom package/source receipt: ' + pattern)
+ shutil.copyfile(files[0], output / files[0].name)
+ for file, name in [(log, 'build.log'), (LOCK_FILE, 'glibc.lock.json'),
+ (source / 'debian/copyright', 'COPYRIGHT'),
+ (source / 'debian/control', 'generated-debian-control'),
+ (source / tests['report'], 'tests.sum'),
+ (source / 'build-tree/amd64-libc/configparms', 'native-configparms'),
+ (source / 'build-tree/amd64-libc/config.make', 'native-config.make')]:
+ shutil.copyfile(file, output / name)
+ for name in CONTROLS:
+ for suffix in ['.out', '.test-result']:
+ report = source / 'build-tree/amd64-libc' / (name + suffix)
+ require(report.is_file(), 'Missing native control output: ' + name + suffix)
+ shutil.copyfile(report, output / (Path(name).name + suffix))
+ (output / 'builder-packages.tsv').write_text(capture(
+ ['dpkg-query', '--show', '--showformat=${binary:Package}\t${Version}\n'], work, environment) + '\n', encoding='utf-8')
+ provenance = {'schemaVersion': 1, 'identity': lock['identity'], 'sourceLockSha256': digest(LOCK_FILE),
+ 'sourceDateEpoch': lock['sourceDateEpoch'], 'sourceFiles': lock['sourceFiles'],
+ 'upstream': lock['upstream'], 'patches': lock['patches'], 'fixedCves': lock['fixedCves'],
+ 'debianPackagingInputSha256': packaging, 'approvedPackagingSha256': approved_packaging,
+ 'generatedControlMetadata': generated_control,
+ 'sourceClosure': source_proof, 'buildEnvironment': environment, 'buildCommand': command,
+ 'dpkgBuildflagsContext': capture(['dpkg-buildflags', '--export=sh'], work, environment),
+ 'actualCompilerConfiguration': ['native-configparms', 'native-config.make', 'build.log'],
+ 'tests': tests, 'runtimePackages': [i for i in selected if i['role'] == 'runtime'],
+ 'compatibilityPackages': [i for i in selected if i['role'] != 'runtime'],
+ 'devPackagePolicy': 'Explicitly requested exports only; consumers may install a dev package only if already present. This helper installs no packages.',
+ 'scope': 'Native amd64 Ubuntu source packaging and original control templates; first-party metadata generated by original rules; nobiarch/noudeb; full native upstream suite and original expected-failure/ABI/hardening rules.',
+ 'signaturePolicy': 'Official input bytes SHA-256 pinned; custom outputs are not issued or signed by Canonical.'}
+ (output / 'provenance.json').write_text(json.dumps(provenance, indent=2) + '\n', encoding='utf-8')
+ (output / 'manifest.tsv').write_text('lock-sha256\t' + digest(LOCK_FILE) + '\n' + ''.join(
+ '\t'.join([i['package'], i['version'], i['architecture'], i['sha256'], i['file']]) + '\n' for i in selected), encoding='utf-8')
+ (output / 'SHA256SUMS').write_text(''.join(digest(f) + ' ' + f.name + '\n' for f in sorted(output.iterdir()) if f.is_file()), encoding='utf-8')
+ print('GLIBC_BACKPORT_BUILT packages=' + str(len(selected)) + ' lock=' + digest(LOCK_FILE), flush=True)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--validate-lock', action='store_true')
+ parser.add_argument('--output-dir', type=Path)
+ parser.add_argument('--work-dir', type=Path)
+ parser.add_argument('--cache-dir', type=Path)
+ parser.add_argument('--jobs', type=int, choices=(2, 4), default=2)
+ parser.add_argument('--export-dev-package', action='append', choices=DEV_PACKAGES, default=[])
+ args = parser.parse_args()
+ require(1 <= args.jobs <= 32, 'Build jobs must be between 1 and 32')
+ require(len(set(args.export_dev_package)) == len(args.export_dev_package), 'Duplicate dev export')
+ lock = load_lock(LOCK_FILE)
+ if args.validate_lock:
+ print('GLIBC_BACKPORT_LOCK_OK sha256=' + digest(LOCK_FILE))
+ else:
+ require(args.output_dir is not None, '--output-dir is required for a native build')
+ build(args, lock)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
+ print('GLIBC_BACKPORT_FAILED: ' + str(error), file=sys.stderr)
+ sys.exit(1)
diff --git a/server/security/ubuntu-backports/glibc/glibc.lock.json b/server/security/ubuntu-backports/glibc/glibc.lock.json
new file mode 100644
index 0000000000..670912dccf
--- /dev/null
+++ b/server/security/ubuntu-backports/glibc/glibc.lock.json
@@ -0,0 +1,87 @@
+{
+ "schemaVersion": 1,
+ "sourcePackage": "glibc",
+ "ubuntuSourceVersion": "2.43-2ubuntu2.4",
+ "customVersion": "2.43-2ubuntu2.4+pasturestack.1",
+ "distribution": "resolute",
+ "osVersion": "26.04",
+ "architecture": "amd64",
+ "sourceDateEpoch": 1792857600,
+ "sourcePublication": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/18721986",
+ "sourceFiles": [
+ {
+ "sha256": "da10d551ca51076bf7bf663de38369d04e267386b8211c283c2814e68e482ed3",
+ "size": 9078,
+ "name": "glibc_2.43-2ubuntu2.4.dsc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.dsc"
+ },
+ {
+ "sha256": "d9c86c6b5dbddb43a3e08270c5844fc5177d19442cf5b8df4be7c07cd5fa3831",
+ "size": 20297012,
+ "name": "glibc_2.43.orig.tar.xz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43.orig.tar.xz"
+ },
+ {
+ "sha256": "0a725eb20ceb526abf52d3129f03e0399afa0d97ffa1dd779bed42958ab23326",
+ "size": 1018,
+ "name": "glibc_2.43.orig.tar.xz.asc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43.orig.tar.xz.asc"
+ },
+ {
+ "sha256": "28103a7cf808c29901c6053d89a4e8299880abfbb850c0d97a09c8df8533e306",
+ "size": 530764,
+ "name": "glibc_2.43-2ubuntu2.4.debian.tar.xz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.debian.tar.xz"
+ }
+ ],
+ "upstream": {
+ "fixCommit": "72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08",
+ "repository": "https://sourceware.org/git/glibc.git",
+ "advisory": "https://sourceware.org/git/?p=glibc.git;a=blob;f=advisories/GLIBC-SA-2026-0015",
+ "backportBranch": "release/2.43/master",
+ "regressionCommit": "20b3e8717652bdc2f89c99c4cc6e38d56a754866",
+ "ubuntuCve": "https://ubuntu.com/security/CVE-2026-18374"
+ },
+ "patches": [
+ {
+ "sha256": "73ca6cb57d78c89785cd2bdd5c15cf7e5fdee873599bb8f1623035a2692915f1",
+ "size": 2175,
+ "name": "72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08.sourceware.patch",
+ "commit": "72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08",
+ "url": "https://sourceware.org/git/?p=glibc.git;a=patch;h=72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08"
+ },
+ {
+ "sha256": "bf4de5ac1937b76fd21ce003c77f5681b899edb8a42326ff3a13bf1ba4ab4ba1",
+ "size": 3183,
+ "name": "20b3e8717652bdc2f89c99c4cc6e38d56a754866.sourceware.patch",
+ "commit": "20b3e8717652bdc2f89c99c4cc6e38d56a754866",
+ "url": "https://sourceware.org/git/?p=glibc.git;a=patch;h=20b3e8717652bdc2f89c99c4cc6e38d56a754866"
+ }
+ ],
+ "buildProfiles": [
+ "nobiarch",
+ "noudeb"
+ ],
+ "runtimePackages": [
+ "libc6",
+ "libc-bin",
+ "libc-gconv-modules-extra"
+ ],
+ "optionalDevPackages": [
+ "libc6-dev",
+ "libc-dev-bin"
+ ],
+ "packagingMetadataPolicy": {
+ "originalControlSha256": "80ad63c9edb6751e404e3a1b05d453433111b307ec00893380487df0534f6999",
+ "generatedControlSha256": "a1cca0ba6356e1318f218b05dec1213bae7d43dce2bd79055119d75070a9b85e",
+ "approvedChangedFields": [
+ "Maintainer",
+ "XSBC-Original-Maintainer"
+ ],
+ "scope": "Preserve Ubuntu source packaging and original control templates. First-party metadata is generated by the original make rules; not issued by Canonical. All other packaging inputs and the final generated control remain hash-bound."
+ },
+ "fixedCves": [
+ "CVE-2026-18374"
+ ],
+ "identity": "First-party PastureStack rebuild of official Ubuntu source packaging and original control templates with the official glibc 2.43 fix and regression backport; metadata generated by original make rules; not issued by Canonical."
+}
diff --git a/server/security/ubuntu-backports/glibc/test_glibc_backport.py b/server/security/ubuntu-backports/glibc/test_glibc_backport.py
new file mode 100644
index 0000000000..f9510ca943
--- /dev/null
+++ b/server/security/ubuntu-backports/glibc/test_glibc_backport.py
@@ -0,0 +1,280 @@
+"""Network-free input, negative boundary and CLI checks; no glibc compilation."""
+import copy
+import hashlib
+import importlib.util
+import io
+import json
+import os
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import unittest
+from unittest import mock
+import urllib.request
+
+HERE = Path(__file__).resolve().parent
+sys.dont_write_bytecode = True
+SPEC = importlib.util.spec_from_file_location('glibc_backport', HERE / 'build-glibc-backport.py')
+MODULE = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(MODULE)
+
+
+class GlibcBackport(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix='glibc-backport-contract-')
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name)
+ self.lock = MODULE.load_lock(HERE / 'glibc.lock.json')
+
+ def load(self, value):
+ file = self.root / 'fixture.lock.json'
+ file.write_text(json.dumps(value), encoding='utf-8')
+ return MODULE.load_lock(file)
+
+ def test_lock_has_exact_source_fix_regression_and_native_boundary(self):
+ self.assertEqual(4, len(self.lock['sourceFiles']))
+ self.assertEqual([MODULE.FIX, MODULE.REGRESSION], [p['commit'] for p in self.lock['patches']])
+ self.assertEqual(['nobiarch', 'noudeb'], self.lock['buildProfiles'])
+ self.assertEqual(['CVE-2026-18374'], self.lock['fixedCves'])
+
+ def test_foreign_identity_architecture_profile_or_runtime_set_fails(self):
+ for field, value in [('sourcePackage', 'eglibc'), ('ubuntuSourceVersion', '2.44-1'),
+ ('customVersion', '2.43-2ubuntu2.4'), ('architecture', 'arm64'),
+ ('buildProfiles', ['nocheck']), ('runtimePackages', ['libc6']),
+ ('optionalDevPackages', ['libc6-dev']), ('sourceDateEpoch', True),
+ ('identity', 'Official Canonical update')]:
+ with self.subTest(field=field):
+ lock = copy.deepcopy(self.lock); lock[field] = value
+ with self.assertRaises(ValueError): self.load(lock)
+
+ def test_missing_duplicate_or_tampered_source_pin_fails(self):
+ for mutation in ['missing', 'duplicate', 'sha256', 'size', 'url', 'name']:
+ lock = copy.deepcopy(self.lock)
+ if mutation == 'missing': lock['sourceFiles'].pop()
+ elif mutation == 'duplicate': lock['sourceFiles'][1] = lock['sourceFiles'][0]
+ else: lock['sourceFiles'][0][mutation] = {'sha256': '0' * 64, 'size': 9079,
+ 'url': 'https://example.invalid/source', 'name': '../outside.dsc'}[mutation]
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError): self.load(lock)
+
+ def test_other_branch_reordered_missing_or_rehashed_patch_fails(self):
+ for mutation in ['branch', 'order', 'missing', 'duplicate', 'sha256', 'url']:
+ lock = copy.deepcopy(self.lock)
+ if mutation == 'branch': lock['patches'][0]['commit'] = '9765a538ebf8661a6e5578e01e35a3dd30db7eb4'
+ if mutation == 'order': lock['patches'].reverse()
+ if mutation == 'missing': lock['patches'].pop()
+ if mutation == 'duplicate': lock['patches'][1] = lock['patches'][0]
+ if mutation == 'sha256': lock['patches'][0]['sha256'] = '0' * 64
+ if mutation == 'url': lock['patches'][0]['url'] = 'http://sourceware.org/patch'
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError): self.load(lock)
+
+ def test_dsc_binds_quilt_format_and_all_three_original_inputs(self):
+ text = 'Format: 3.0 (quilt)\nSource: glibc\nVersion: 2.43-2ubuntu2.4\nChecksums-Sha256:\n' + ''.join(
+ ' {sha256} {size} {name}\n'.format(**item) for item in self.lock['sourceFiles'][1:])
+ file = self.root / 'glibc_2.43-2ubuntu2.4.dsc'
+ file.write_text(text, encoding='utf-8'); MODULE.verify_source_control(self.lock, self.root)
+ for bad in [text.replace('3.0 (quilt)', '1.0'), text.replace('Source: glibc', 'Source: other'),
+ text.replace('2.43-2ubuntu2.4\n', '2.44-1\n'), text.rsplit('\n', 2)[0] + '\n',
+ text + text.splitlines()[-1] + '\n',
+ text.replace(self.lock['sourceFiles'][1]['sha256'], '0' * 64)]:
+ file.write_text(bad, encoding='utf-8')
+ with self.assertRaises(ValueError): MODULE.verify_source_control(self.lock, self.root)
+
+ def test_poisoned_cache_fails_without_network_fallback(self):
+ cache, output = self.root / 'cache', self.root / 'input'
+ cache.mkdir(); output.mkdir(); data = b'official pinned bytes\n'
+ item = {'name': 'input.patch', 'url': 'https://example.invalid/never',
+ 'size': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
+ cached = cache / item['name']; cached.write_bytes(data)
+ with mock.patch.object(urllib.request, 'build_opener') as network:
+ self.assertEqual(data, MODULE.download(item, output, cache).read_bytes())
+ cached.write_bytes(data + b'tampered')
+ with self.assertRaises(ValueError): MODULE.download(item, output, cache)
+ network.assert_not_called()
+
+ def test_download_limit_uses_exact_pin_instead_of_generic_cap(self):
+ class Response(io.BytesIO):
+ url = 'https://sourceware.org/patch'
+ item = {'name': 'input.patch', 'url': Response.url, 'size': 3,
+ 'sha256': hashlib.sha256(b'abc').hexdigest()}
+ opener = mock.Mock(); opener.open.return_value = Response(b'abcd')
+ with mock.patch.object(urllib.request, 'build_opener', return_value=opener):
+ with self.assertRaisesRegex(ValueError, 'pinned size'): MODULE.download(item, self.root, None)
+
+ def test_https_redirect_downgrade_fails(self):
+ with self.assertRaises(ValueError):
+ MODULE.COMMON.HttpsRedirect().redirect_request(urllib.request.Request('https://sourceware.org/patch'),
+ None, 302, 'Found', {}, 'http://example.invalid/patch')
+
+ def test_patch_commit_paths_metadata_and_bytes_are_verified(self):
+ file = self.root / 'fix.patch'
+ original = 'From ' + MODULE.FIX + ' Mon Sep 17 00:00:00 2001\ndiff --git a/libio/fileops.c b/libio/fileops.c\n'
+ for text, valid in [(original, True), (original.replace(MODULE.FIX, '0' * 40), False),
+ (original.replace('libio/fileops.c', 'debian/rules'), False),
+ (original.replace('libio/fileops.c', 'libio/../../outside'), False),
+ (original + 'new mode 100755\n', False)]:
+ file.write_text(text, encoding='utf-8')
+ item = {'name': file.name, 'commit': MODULE.FIX, 'size': len(file.read_bytes()),
+ 'sha256': MODULE.digest(file)}
+ if valid: MODULE.verify_patch(item, file)
+ else:
+ with self.assertRaises(ValueError): MODULE.verify_patch(item, file)
+ file.write_text(original, encoding='utf-8')
+ with self.assertRaises(ValueError): MODULE.verify_patch(item, file)
+
+ def test_environment_cannot_skip_tests_replace_compilers_or_expand_abis(self):
+ with mock.patch.dict(os.environ, {'DEB_BUILD_OPTIONS': 'nocheck noopt', 'DEB_BUILD_PROFILES': 'stage1',
+ 'RUN_TESTSUITE': 'no', 'CC': 'unsafe', 'CFLAGS': '-O0'}):
+ environment = MODULE.build_environment(self.lock, 4)
+ self.assertEqual('parallel=4', environment['DEB_BUILD_OPTIONS'])
+ self.assertEqual('nobiarch noudeb', environment['DEB_BUILD_PROFILES'])
+ self.assertEqual('hardening=+all', environment['DEB_BUILD_MAINT_OPTIONS'])
+ for name in ['RUN_TESTSUITE', 'CC', 'CFLAGS']: self.assertNotIn(name, environment)
+
+ def test_source_closure_rejects_old_fallback_or_unregistered_regression(self):
+ directory = self.root / 'libio'; directory.mkdir()
+ source = directory / 'fileops.c'; source.write_text(MODULE.NEW_CALL, encoding='utf-8')
+ (directory / 'Makefile').write_text('tst-fopen-ccs-empty', encoding='utf-8')
+ (directory / 'tst-fopen-ccs-empty.c').write_text('official fixture', encoding='utf-8')
+ self.assertEqual(3, len(MODULE.source_closure(self.root)))
+ source.write_text(MODULE.NEW_CALL + MODULE.OLD_CALL, encoding='utf-8')
+ with self.assertRaises(ValueError): MODULE.source_closure(self.root)
+ source.write_text(MODULE.NEW_CALL, encoding='utf-8')
+ (directory / 'Makefile').write_text('', encoding='utf-8')
+ with self.assertRaises(ValueError): MODULE.source_closure(self.root)
+
+ def native_fixture(self):
+ directory = self.root / 'build-tree/amd64-libc'; directory.mkdir(parents=True)
+ (self.root / 'stamp-dir').mkdir()
+ (self.root / 'stamp-dir/check_libc_passed').touch()
+ text = '\t\t=== glibc tests ===\n\nRunning . ...\n\n' + ''.join('PASS: ' + name + '\n' for name in MODULE.CONTROLS) + ''.join(
+ 'PASS: ' + component + '/ordinary-control\n' for component in
+ ['stdio-common', 'iconv', 'wcsmbs', 'malloc', 'elf', 'nptl', 'nss'])
+ text += 'XFAIL: elf/known-failure\nUNSUPPORTED: nss/unsupported-environment\nXPASS: elf/known-xpass\n'
+ file = directory / 'tests.sum'; file.write_text(text, encoding='utf-8')
+ return file, text
+
+ def test_full_native_receipt_preserves_xfail_unsupported_xpass_semantics(self):
+ self.native_fixture()
+ result = MODULE.test_receipt(self.root)
+ self.assertEqual({'PASS': 10, 'XFAIL': 1, 'UNSUPPORTED': 1, 'XPASS': 1}, result['counts'])
+
+ def test_native_summary_requires_canonical_framing_and_rejects_unknown_lines(self):
+ good = '\t\t=== glibc tests ===\n\nRunning libio ...\nPASS: libio/control\n'
+ self.assertEqual([('PASS', 'libio/control')], MODULE.parse_test_summary(good))
+ for bad in [good.replace('=== glibc tests ===', '=== another suite ==='),
+ good.replace('Running libio ...', 'Running libio unexpectedly'),
+ good + 'unexpected diagnostic\n', good + 'Running libio ...\n',
+ good + 'SKIP: libio/unknown-state\n', 'PASS: libio/control\n']:
+ with self.subTest(summary=bad), self.assertRaises(ValueError): MODULE.parse_test_summary(bad)
+
+ def packaging_fixture(self):
+ debian = self.root / 'debian'
+ (debian / 'control.in').mkdir(parents=True)
+ (debian / 'rules.d').mkdir()
+ for name, text in [('control', 'Maintainer: Ubuntu Developers\n'),
+ ('control.in/main', 'Maintainer: GNU Libc Maintainers\n'),
+ ('rules', 'original rules\n'), ('rules.d/control.mk', 'original control generator\n')]:
+ (debian / name).write_text(text, encoding='utf-8')
+ return debian
+
+ def test_original_control_metadata_remains_bound_to_the_packaging_snapshot(self):
+ debian = self.packaging_fixture()
+ approved = MODULE.COMMON.packaging_snapshot(self.root)
+ MODULE.verify_packaging_inputs(self.root, approved)
+ (debian / 'control').write_bytes((debian / 'control.in/main').read_bytes())
+ with self.assertRaisesRegex(ValueError, 'debian/control'):
+ MODULE.verify_packaging_inputs(self.root, approved)
+
+ def test_generated_control_only_accepts_the_approved_original_template_bytes(self):
+ debian = self.packaging_fixture()
+ original = MODULE.digest(debian / 'control')
+ generated = MODULE.digest(debian / 'control.in/main')
+ def generate(*_):
+ (debian / 'control').write_bytes((debian / 'control.in/main').read_bytes())
+ with mock.patch.object(MODULE, 'CONTROL_ORIGINAL_SHA256', original), \
+ mock.patch.object(MODULE, 'CONTROL_GENERATED_SHA256', generated), \
+ mock.patch.object(MODULE, 'run', side_effect=generate):
+ receipt = MODULE.prepare_generated_control(self.root, {}, self.root / 'build.log')
+ self.assertEqual(original, receipt['originalSha256'])
+ self.assertEqual(generated, receipt['generatedSha256'])
+ self.assertIn('debian/control.in/main', receipt['inputSha256'])
+ approved = MODULE.COMMON.packaging_snapshot(self.root)
+ MODULE.verify_packaging_inputs(self.root, approved)
+ with self.assertRaisesRegex(ValueError, 'Fresh original'):
+ MODULE.prepare_generated_control(self.root, {}, self.root / 'build.log')
+
+ def test_control_generation_rejects_extra_field_or_packaging_changes(self):
+ debian = self.packaging_fixture()
+ original_bytes = (debian / 'control').read_bytes()
+ original, generated = MODULE.digest(debian / 'control'), MODULE.digest(debian / 'control.in/main')
+ for change_other in [False, True]:
+ (debian / 'control').write_bytes(original_bytes)
+ def generate(*_):
+ (debian / 'control').write_bytes((debian / 'control.in/main').read_bytes())
+ if change_other:
+ (debian / 'rules').write_text('changed rules\n', encoding='utf-8')
+ else:
+ with (debian / 'control').open('a', encoding='utf-8') as stream:
+ stream.write('Depends: unapproved\n')
+ with mock.patch.object(MODULE, 'CONTROL_ORIGINAL_SHA256', original), \
+ mock.patch.object(MODULE, 'CONTROL_GENERATED_SHA256', generated), \
+ mock.patch.object(MODULE, 'run', side_effect=generate), \
+ self.assertRaisesRegex(ValueError, 'debian/rules|approved original-template'):
+ MODULE.prepare_generated_control(self.root, {}, self.root / 'build.log')
+
+ def test_generated_control_policy_cannot_be_widened(self):
+ for field, value in [('originalControlSha256', '0' * 64),
+ ('generatedControlSha256', '0' * 64),
+ ('approvedChangedFields', ['Maintainer', 'Depends'])]:
+ lock = copy.deepcopy(self.lock)
+ lock['packagingMetadataPolicy'][field] = value
+ with self.subTest(field=field), self.assertRaises(ValueError): self.load(lock)
+
+ def test_native_failure_skipped_cve_component_or_extra_abi_fails(self):
+ file, text = self.native_fixture()
+ for bad in [text + 'FAIL: libio/surviving-bug\n', text + 'ERROR: elf/error\n',
+ text + 'NEW_UNKNOWN_STATE: libio/unknown\n',
+ text.replace('PASS: ' + MODULE.CONTROLS[0], 'UNSUPPORTED: ' + MODULE.CONTROLS[0]),
+ text.replace('PASS: malloc/ordinary-control\n', ''), text + 'PASS: ' + MODULE.CONTROLS[0] + '\n']:
+ file.write_text(bad, encoding='utf-8')
+ with self.assertRaises(ValueError): MODULE.test_receipt(self.root)
+ file.write_text(text, encoding='utf-8'); (self.root / 'stamp-dir/check_libc_failed').touch()
+ with self.assertRaises(ValueError): MODULE.test_receipt(self.root)
+ (self.root / 'stamp-dir/check_libc_failed').unlink()
+ (self.root / 'build-tree/amd64-x32').mkdir()
+ with self.assertRaises(ValueError): MODULE.test_receipt(self.root)
+
+ def test_original_packaging_and_disjoint_output_guards_are_reused(self):
+ debian = self.root / 'debian'; debian.mkdir()
+ (debian / 'rules').write_text('original tests and flags', encoding='utf-8')
+ (debian / 'changelog').write_text('original', encoding='utf-8')
+ snapshot = MODULE.COMMON.packaging_snapshot(self.root)
+ (debian / 'changelog').write_text('custom version', encoding='utf-8')
+ self.assertEqual(snapshot, MODULE.COMMON.packaging_snapshot(self.root))
+ (debian / 'rules').write_text('nocheck', encoding='utf-8')
+ self.assertNotEqual(snapshot, MODULE.COMMON.packaging_snapshot(self.root))
+ for directory in [self.root / 'out', self.root / 'out/nested', self.root]:
+ with self.assertRaises(ValueError):
+ MODULE.COMMON.require_separate_paths(self.root / 'out', directory, None)
+
+ def cli(self, *args):
+ return subprocess.run([sys.executable, '-B', str(HERE / 'build-glibc-backport.py'), *args],
+ capture_output=True, text=True, encoding='utf-8')
+
+ def test_cli_validate_lock_is_network_and_linux_build_independent(self):
+ result = self.cli('--validate-lock')
+ self.assertEqual(0, result.returncode, result.stderr)
+ self.assertIn('GLIBC_BACKPORT_LOCK_OK', result.stdout)
+ self.assertNotIn('GLIBC_BACKPORT_BUILT', result.stdout)
+
+ def test_cli_rejects_duplicate_foreign_dev_export_invalid_jobs_and_external_input_mode(self):
+ for args in [('--validate-lock', '--jobs', '0'), ('--validate-lock', '--jobs', '33'),
+ ('--validate-lock', '--export-dev-package', 'libc6-dev', '--export-dev-package', 'libc6-dev'),
+ ('--validate-lock', '--export-dev-package', 'gcc'), ('--validate-inputs',),
+ ('--validate-lock', '--lock', 'untrusted.json')]:
+ with self.subTest(args=args): self.assertNotEqual(0, self.cli(*args).returncode)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/server/security/ubuntu-backports/jq/build-jq-release.py b/server/security/ubuntu-backports/jq/build-jq-release.py
new file mode 100644
index 0000000000..2f610ed656
--- /dev/null
+++ b/server/security/ubuntu-backports/jq/build-jq-release.py
@@ -0,0 +1,305 @@
+#!/usr/bin/env python3
+"""Build official stable jq 1.8.2 using preserved Ubuntu 26.04 packaging.
+
+Run non-root in an isolated native amd64 builder with the original Build-Depends.
+Only changelog and already-upstream security-patch series entries are adjusted.
+No runtime installation occurs. Native full tests, ABI, system-onig linkage and
+extracted-package smoke must pass before exporting the two runtime packages.
+"""
+import argparse
+import datetime
+import email.utils
+import importlib.util
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+HERE = Path(__file__).resolve().parent
+LOCK = HERE / 'jq.lock.json'
+sys.dont_write_bytecode = True
+SPEC = importlib.util.spec_from_file_location('pcre2_source_helpers', HERE.parent / 'build-pcre2-backport.py')
+C = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(C)
+require, digest, download, run, capture = C.require, C.digest, C.download, C.run, C.capture
+UBUNTU = '1.8.1-4ubuntu2'
+VERSION = '1.8.2-0pasturestack1~26.04.1'
+KEPT = ['get-version.patch', 'dont-distribute-duplicate-docs.patch',
+ '0008-Do-not-use-venderized-oniguruma.patch', '0006-Do-not-use-pipenv-to-build-docs.patch']
+DROPPED = ['CVE-2026-' + x + '.patch' for x in ['32316', '33947', '33948', '39956', '39979', '40164']]
+TESTS = ['mantest', 'jqtest', 'shtest', 'utf8test', 'base64test', 'uritest',
+ 'optionaltest', 'onigtest', 'manonigtest']
+
+
+def load_lock():
+ lock = json.loads(LOCK.read_text(encoding='utf-8'))
+ expected = {'schemaVersion': 1, 'sourcePackage': 'jq', 'ubuntuSourceVersion': UBUNTU,
+ 'upstreamVersion': '1.8.2', 'customVersion': VERSION, 'distribution': 'resolute',
+ 'osVersion': '26.04', 'architecture': 'amd64', 'runtimePackages': ['jq', 'libjq1'],
+ 'owningTests': TESTS}
+ require(all(lock.get(k) == v for k, v in expected.items()), 'Unexpected jq lock identity')
+ require(type(lock['sourceDateEpoch']) is int and lock['sourceDateEpoch'] > 0,
+ 'Invalid source date')
+ source_names = {'jq_' + UBUNTU + '.dsc', 'jq_1.8.1.orig.tar.gz',
+ 'jq_' + UBUNTU + '.debian.tar.xz'}
+ require(len(lock['sourceFiles']) == 3 and
+ {x['name'] for x in lock['sourceFiles']} == source_names, 'Wrong Ubuntu source inputs')
+ require([p['name'] for p in lock['retainedUbuntuPatches']] == KEPT and
+ [p['patch'] for p in lock['removedUbuntuPatches']] == DROPPED,
+ 'Unexpected Ubuntu packaging patch boundary')
+ upstream = lock['upstream']
+ require(upstream['commit'] == '34f7186b86743a083a589741b6cea95293524108'
+ and upstream['tag'] == 'jq-1.8.2'
+ and upstream['name'] == 'jq-1.8.2.tar.gz', 'Unexpected official stable source')
+ require(len(upstream['releaseSecurityCves']) == 16 and
+ set(upstream['additionalAdvisories']) ==
+ {'GHSA-gf4g-95wj-4q4r', 'GHSA-hj52-j2c9-r8r4'}, 'Incomplete release security inventory')
+ for item in lock['sourceFiles'] + [upstream, upstream['checksumFile']]:
+ require(re.fullmatch(r'[A-Za-z0-9._~+-]+', item['name'])
+ and re.fullmatch(r'[0-9a-f]{64}', item['sha256'])
+ and type(item['size']) is int and 0 < item['size'] < 8 * 1024 * 1024,
+ 'Unpinned or unsafe source')
+ expected_url = ('https://archive.ubuntu.com/ubuntu/pool/main/j/jq/' + item['name']
+ if item in lock['sourceFiles'] else
+ 'https://github.com/jqlang/jq/releases/download/jq-1.8.2/' +
+ ('sha256sum.txt' if item == upstream['checksumFile'] else item['name']))
+ require(item['url'] == expected_url, 'Unexpected source URL')
+ for proof in lock['removedUbuntuPatches']:
+ require(proof['status'] == 'ahead' and proof['mergeBase'] == proof['officialFixCommit']
+ and proof['officialReleaseCommit'] == upstream['commit']
+ and re.fullmatch(r'[0-9a-f]{40}', proof['officialFixCommit'])
+ and re.fullmatch(r'[0-9a-f]{64}', proof['originalPatchSha256']),
+ 'Old security patch lacks official inclusion proof')
+ for group in ['releaseSourceSha256', 'ubuntuPackagingSha256', 'postUbuntuPatchSha256']:
+ for name, sha in lock[group].items():
+ require(not Path(name).is_absolute() and '..' not in Path(name).parts
+ and re.fullmatch(r'[0-9a-f]{64}', sha), 'Unsafe/unpinned source path')
+ return lock
+
+
+def verify_hashes(root, expected):
+ require(all((root / name).is_file() and digest(root / name) == sha
+ for name, sha in expected.items()), 'Source or packaging digest mismatch')
+
+
+def verify_inputs(lock, inputs):
+ for item in lock['sourceFiles'] + [lock['upstream'], lock['upstream']['checksumFile']]:
+ file = inputs / item['name']
+ require(file.is_file() and file.stat().st_size == item['size']
+ and digest(file) == item['sha256'], 'Pinned input mismatch: ' + item['name'])
+ text = (inputs / ('jq_' + UBUNTU + '.dsc')).read_text(encoding='utf-8')
+ require(all(re.search('^' + re.escape(line) + '$', text, re.M) for line in
+ ['Format: 3.0 (quilt)', 'Source: jq', 'Version: ' + UBUNTU]), 'Wrong Ubuntu source')
+ block = re.search(r'^Checksums-Sha256:\n((?: .+\n)+)', text, re.M)
+ require(block, 'Missing .dsc SHA-256 manifest')
+ actual = {n: (h, int(s)) for h, s, n in
+ re.findall(r'^ ([0-9a-f]{64}) (\d+) (\S+)$', block[1], re.M)}
+ require(actual == {x['name']: (x['sha256'], x['size']) for x in lock['sourceFiles']
+ if not x['name'].endswith('.dsc')}, 'Ubuntu .dsc/lock manifest mismatch')
+ text = (inputs / lock['upstream']['checksumFile']['name']).read_text(encoding='utf-8')
+ require(re.search(r'^' + lock['upstream']['sha256'] +
+ r'\s+\*?jq-1\.8\.2\.tar\.gz$', text, re.M),
+ 'Official release checksum list disagrees')
+
+
+def safe_unpack(archive, work):
+ with tarfile.open(archive) as source:
+ require(all(Path(p.name).parts and Path(p.name).parts[0] == 'jq-1.8.2'
+ and not Path(p.name).is_absolute() and '..' not in Path(p.name).parts
+ for p in source.getmembers()), 'Unsafe release archive layout')
+ source.extractall(work, filter='data')
+ return work / 'jq-1.8.2'
+
+
+def prepare(lock, inputs, work, env, log):
+ old = work / 'ubuntu-packaging-source'
+ run(['dpkg-source', '--extract', str(inputs / ('jq_' + UBUNTU + '.dsc')), str(old)],
+ work, env, log)
+ verify_hashes(old, lock['ubuntuPackagingSha256'])
+ for patch in lock['retainedUbuntuPatches']:
+ require(digest(old / 'debian/patches' / patch['name']) == patch['sha256'],
+ 'Ubuntu packaging patch changed')
+ for proof in lock['removedUbuntuPatches']:
+ require(digest(old / 'debian/patches' / proof['patch']) == proof['originalPatchSha256'],
+ 'Old Ubuntu security patch changed')
+ source = safe_unpack(inputs / lock['upstream']['name'], work)
+ verify_hashes(source, lock['releaseSourceSha256'])
+ shutil.copytree(old / 'debian', source / 'debian', symlinks=True)
+ series = source / 'debian/patches/series'
+ require(series.read_text(encoding='utf-8').splitlines() == KEPT + DROPPED,
+ 'Unexpected original Ubuntu series')
+ series.write_text(''.join(p + '\n' for p in KEPT), encoding='utf-8')
+ changelog = source / 'debian/changelog'
+ date = email.utils.format_datetime(datetime.datetime.fromtimestamp(
+ lock['sourceDateEpoch'], datetime.timezone.utc))
+ changelog.write_text('jq (' + VERSION + ') resolute; urgency=high\n\n'
+ ' * First-party PastureStack rebuild; not issued by Canonical.\n'
+ ' * Use official stable jq 1.8.2, including all published security fixes.\n'
+ ' * Preserve Ubuntu packaging; omit six series entries already upstream.\n\n'
+ ' -- PastureStack maintainers ' + date +
+ '\n\n' + changelog.read_text(encoding='utf-8'), encoding='utf-8')
+ # dpkg-source uses zero-fuzz quilt application and tracks the retained patches.
+ run(['dpkg-source', '--before-build', str(source)], work, env, log)
+ verify_hashes(source, lock['postUbuntuPatchSha256'])
+ original = C.packaging_snapshot(old)
+ original.pop('debian/patches/series')
+ verify_hashes(source, original)
+ return source, C.packaging_snapshot(source)
+
+
+def test_receipts(source):
+ receipts = []
+ for name in TESTS:
+ reports = list(source.rglob(name + '.trs'))
+ require(len(reports) == 1, 'Missing/ambiguous owning test receipt: ' + name)
+ report = reports[0]
+ require(re.search(r'^:test-result: PASS$', report.read_text(encoding='utf-8'), re.M),
+ 'Native owning test did not PASS: ' + name)
+ receipts.append({'test': name, 'status': 'PASS', 'path': report.relative_to(source).as_posix(),
+ 'sha256': digest(report)})
+ return receipts
+
+
+def verify_source_identity(package, value):
+ require(value in ['jq', 'jq (' + VERSION + ')'] or (package == 'jq' and value == ''),
+ 'Source package identity changed')
+
+
+def verify_packages(lock, source, work, env, log):
+ extracted = work / 'extracted'
+ selected = []
+ for package in ['jq', 'libjq1']:
+ files = list(work.glob(package + '_' + VERSION + '_amd64.deb'))
+ require(len(files) == 1, 'Missing/ambiguous package: ' + package)
+ file = files[0]
+ for field, expected in [('Package', package), ('Version', VERSION), ('Architecture', 'amd64')]:
+ require(capture(['dpkg-deb', '--field', str(file), field], work, env) == expected,
+ 'Package identity changed: ' + field)
+ verify_source_identity(package, capture(['dpkg-deb', '--field', str(file), 'Source'], work, env))
+ run(['dpkg-deb', '--extract', str(file), str(extracted)], work, env, log)
+ selected.append({'package': package, 'version': VERSION, 'architecture': 'amd64',
+ 'file': file.name, 'sha256': digest(file)})
+ binary, library = extracted / 'usr/bin/jq', extracted / 'usr/lib/x86_64-linux-gnu/libjq.so.1'
+ require(binary.is_file() and library.is_file(), 'Expected runtime entry point/SONAME missing')
+ dynamic = capture(['readelf', '--dynamic', str(library)], work, env)
+ require('[libjq.so.1]' in dynamic and re.search(r'NEEDED.*\[libonig\.so\.5\]', dynamic),
+ 'libjq ABI/system-oniguruma linkage changed')
+ required = re.findall(r'^ ([A-Za-z0-9_]+)@Base ', (source / 'debian/libjq1.symbols').read_text(), re.M)
+ symbols = capture(['nm', '--dynamic', '--defined-only', str(library)], work, env)
+ exports = {line.split()[-1].split('@')[0] for line in symbols.splitlines() if line.split()}
+ require(required and set(required) <= exports, 'An original Ubuntu public ABI symbol is missing')
+ for file in [binary, library]:
+ dyn = capture(['readelf', '--dynamic', str(file)], work, env)
+ headers = capture(['readelf', '--program-headers', str(file)], work, env)
+ require('GNU_RELRO' in headers and ('BIND_NOW' in dyn or re.search(r'FLAGS_1.*NOW', dyn)),
+ 'Ubuntu hardening missing')
+ child_env = dict(env, LD_LIBRARY_PATH=str(library.parent))
+ require(capture([str(binary), '--version'], work, child_env) == 'jq-1.8.2', 'Wrong packaged version')
+ for expression, expected in [('{ok: (1+2)}', '{"ok":3}'),
+ ('"abc" | test("^a")', 'true'),
+ ('{a:{b:1}} * {a:{c:2}}', '{"a":{"b":1,"c":2}}'),
+ ('[1,[2]] | contains([[2]])', 'true')]:
+ require(capture([str(binary), '-nc', expression], work, child_env) == expected,
+ 'Packaged valid-input smoke failed')
+ require(digest(extracted / 'usr/share/doc/jq/copyright') ==
+ digest(source / 'debian/copyright'), 'Copyright changed')
+ return selected, {'validInputCases': 4, 'status': 'PASS', 'publicAbiSymbols': len(required),
+ 'soname': 'libjq.so.1', 'systemOniguruma': 'libonig.so.5'}
+
+
+def build(args, lock):
+ release = dict(re.findall(r'^(\w+)=["\']?([^"\'\n]+)',
+ Path('/etc/os-release').read_text(encoding='utf-8'), re.M))
+ require(release.get('ID') == 'ubuntu' and release.get('VERSION_ID') == '26.04'
+ and os.geteuid() != 0, 'Use a non-root isolated Ubuntu 26.04 native builder')
+ require(sys.version_info >= (3, 12), 'Python 3.12 or later is required for safe tar extraction')
+ output = args.output_dir.resolve()
+ cache = args.cache_dir.resolve() if args.cache_dir else None
+ C.require_separate_paths(output, args.work_dir.resolve() if args.work_dir else None, cache)
+ require(not output.exists(), 'Output directory must be new')
+ work = args.work_dir.resolve() if args.work_dir else Path(tempfile.mkdtemp(prefix='jq-stable-'))
+ if args.work_dir: work.mkdir(parents=True, exist_ok=False)
+ home = work / 'home'
+ home.mkdir(mode=0o700)
+ # CLI job choice never becomes an arbitrary command argument or compiler override.
+ jobs, job_arg = (2, '-j2') if args.jobs == 2 else (4, '-j4')
+ env = C.build_environment(lock, jobs)
+ env.update(HOME=str(home), DEB_BUILD_MAINT_OPTIONS='')
+ for tool in ['dpkg-source', 'dpkg-buildpackage', 'dpkg-checkbuilddeps', 'dpkg-deb', 'dpkg-query',
+ 'readelf', 'nm', 'patch', 'make', 'valgrind']:
+ require(shutil.which(tool, path=env['PATH']), 'Builder dependency missing: ' + tool)
+ require(capture(['dpkg', '--print-architecture'], work, env) == 'amd64', 'Native amd64 required')
+ print('JQ_STABLE_WORK_DIR=' + str(work), flush=True)
+ inputs = work / 'inputs'
+ inputs.mkdir()
+ if cache: cache.mkdir(parents=True, exist_ok=True)
+ for item in lock['sourceFiles'] + [lock['upstream'], lock['upstream']['checksumFile']]:
+ download(item, inputs, cache)
+ verify_inputs(lock, inputs)
+ log = work / 'build.log'
+ source, packaging = prepare(lock, inputs, work, env, log)
+ run(['dpkg-checkbuilddeps'], source, env, log)
+ run(['dpkg-buildpackage', '-b', '-us', '-uc', job_arg], source, env, log)
+ verify_hashes(source, packaging)
+ tests = test_receipts(source)
+ packages, smoke = verify_packages(lock, source, work, env, log)
+ output.mkdir(parents=True)
+ for package in packages:
+ shutil.copyfile(work / package['file'], output / package['file'])
+ shutil.copyfile(LOCK, output / 'jq.lock.json')
+ shutil.copyfile(log, output / 'build.log')
+ shutil.copyfile(source / 'debian/copyright', output / 'COPYRIGHT')
+ for receipt in tests:
+ for suffix in ['.trs', '.log']:
+ report = (source / receipt['path']).with_suffix(suffix)
+ require(report.is_file(), 'Test log missing')
+ shutil.copyfile(report, output / (receipt['test'] + suffix))
+ for pattern in ['jq_*.buildinfo', 'jq_*.changes']:
+ matches = list(work.glob(pattern))
+ require(len(matches) == 1, 'Package build receipt missing/ambiguous')
+ shutil.copyfile(matches[0], output / matches[0].name)
+ (output / 'builder-packages.tsv').write_text(capture(
+ ['dpkg-query', '--show', '--showformat=' + '$' + '{binary:Package}\t' + '$' + '{Version}\n'],
+ work, env) + '\n', encoding='utf-8')
+ provenance = {'schemaVersion': 1, 'identity': lock['identity'], 'sourceLockSha256': digest(LOCK),
+ 'sourceFiles': lock['sourceFiles'], 'upstream': lock['upstream'],
+ 'sharedHelpersSha256': digest(HERE.parent / 'build-pcre2-backport.py'),
+ 'packagingPreserved': packaging, 'tests': tests, 'packageSmoke': smoke,
+ 'runtimePackages': packages, 'runtimeInstalled': False,
+ 'removedSeriesEntries': lock['removedUbuntuPatches'],
+ 'buildCommand': ['dpkg-buildpackage', '-b', '-us', '-uc', job_arg]}
+ (output / 'provenance.json').write_text(json.dumps(provenance, indent=2) + '\n', encoding='utf-8')
+ (output / 'manifest.tsv').write_text('lock-sha256\t' + digest(LOCK) + '\n' + ''.join(
+ '\t'.join([p['package'], p['version'], p['architecture'], p['sha256'], p['file']]) + '\n'
+ for p in packages), encoding='utf-8')
+ (output / 'SHA256SUMS').write_text(''.join(digest(p) + ' ' + p.name + '\n'
+ for p in sorted(output.iterdir()) if p.is_file()), encoding='utf-8')
+
+
+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()
+ if args.validate_lock:
+ print('JQ_STABLE_LOCK_OK sha256=' + digest(LOCK))
+ return
+ require(args.output_dir is not None, '--output-dir is required for native build')
+ build(args, lock)
+
+
+if __name__ == '__main__':
+ try:
+ main()
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
+ print('JQ_STABLE_FAILED: ' + str(error), file=sys.stderr)
+ sys.exit(1)
diff --git a/server/security/ubuntu-backports/jq/jq.lock.json b/server/security/ubuntu-backports/jq/jq.lock.json
new file mode 100644
index 0000000000..4c3e2eefb5
--- /dev/null
+++ b/server/security/ubuntu-backports/jq/jq.lock.json
@@ -0,0 +1,260 @@
+{
+ "schemaVersion": 1,
+ "sourcePackage": "jq",
+ "ubuntuSourceVersion": "1.8.1-4ubuntu2",
+ "upstreamVersion": "1.8.2",
+ "customVersion": "1.8.2-0pasturestack1~26.04.1",
+ "distribution": "resolute",
+ "osVersion": "26.04",
+ "architecture": "amd64",
+ "runtimePackages": [
+ "jq",
+ "libjq1"
+ ],
+ "identity": "First-party rebuild of official stable jq1.8.2 with Ubuntu packaging; not issued by Canonical.",
+ "sourceDateEpoch": 1792886400,
+ "sourcePublication": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/18373622",
+ "sourceFiles": [
+ {
+ "name": "jq_1.8.1-4ubuntu2.dsc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/j/jq/jq_1.8.1-4ubuntu2.dsc",
+ "sha256": "faffac21d3a1f6fc96815ee630b6e71903db2ca6c354afe87c68464d85a71e08",
+ "size": 1979
+ },
+ {
+ "name": "jq_1.8.1.orig.tar.gz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/j/jq/jq_1.8.1.orig.tar.gz",
+ "sha256": "51c4ec0913281d917f22a823d1ddcf17c9bc2be845fac987d5fe1d3ae78dd6a2",
+ "size": 1409730
+ },
+ {
+ "name": "jq_1.8.1-4ubuntu2.debian.tar.xz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/j/jq/jq_1.8.1-4ubuntu2.debian.tar.xz",
+ "sha256": "7261bb970ddb1a677988606144151a8dc54437066f12e3ebb1496062da50f2c7",
+ "size": 18368
+ }
+ ],
+ "upstream": {
+ "name": "jq-1.8.2.tar.gz",
+ "url": "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-1.8.2.tar.gz",
+ "sha256": "71b8d6e8f5fe81f6c6d0d110e3892251f6ce76ed095abd315e26e6e1193af3af",
+ "size": 1959950,
+ "tag": "jq-1.8.2",
+ "commit": "34f7186b86743a083a589741b6cea95293524108",
+ "releaseURL": "https://github.com/jqlang/jq/releases/tag/jq-1.8.2",
+ "checksumFile": {
+ "name": "jq-1.8.2-sha256sum.txt",
+ "url": "https://github.com/jqlang/jq/releases/download/jq-1.8.2/sha256sum.txt",
+ "sha256": "dc86824a41c165ece971ff691aff6e08bbfe6e1d1f531688b47ee78c283a85cd",
+ "size": 2302
+ },
+ "releaseSecurityCves": [
+ "CVE-2026-32316",
+ "CVE-2026-33947",
+ "CVE-2026-33948",
+ "CVE-2026-39956",
+ "CVE-2026-39979",
+ "CVE-2026-40164",
+ "CVE-2026-40612",
+ "CVE-2026-41256",
+ "CVE-2026-41257",
+ "CVE-2026-43894",
+ "CVE-2026-43895",
+ "CVE-2026-43896",
+ "CVE-2026-44777",
+ "CVE-2026-47770",
+ "CVE-2026-49839",
+ "CVE-2026-54679"
+ ],
+ "additionalAdvisories": [
+ "GHSA-gf4g-95wj-4q4r",
+ "GHSA-hj52-j2c9-r8r4"
+ ]
+ },
+ "retainedUbuntuPatches": [
+ {
+ "name": "get-version.patch",
+ "sha256": "c0bd5f3dca373f87df3610e4510204cd8530dbdb155b0153b28457871b7950fe",
+ "paths": [
+ "scripts/version"
+ ]
+ },
+ {
+ "name": "dont-distribute-duplicate-docs.patch",
+ "sha256": "6386843af061c538768742207d9fc02603f56e678acd31196d438d3519a7eb6b",
+ "paths": [
+ "Makefile.am"
+ ]
+ },
+ {
+ "name": "0008-Do-not-use-venderized-oniguruma.patch",
+ "sha256": "56073768aadc6b776266e078ac53503b0cf710980c911090c4a92d8af68b3da5",
+ "paths": [
+ "Makefile.am"
+ ]
+ },
+ {
+ "name": "0006-Do-not-use-pipenv-to-build-docs.patch",
+ "sha256": "73500f946a485c28fa2f8f100d09cf46b25da9c59b77577b60cdf372b70a9153",
+ "paths": [
+ "Makefile.am",
+ "configure.ac"
+ ]
+ }
+ ],
+ "removedUbuntuPatches": [
+ {
+ "patch": "CVE-2026-32316.patch",
+ "originalPatchSha256": "6e0296c2cbee9607771448d017dd870a5b2f5dd286e14033fac60f5648311da7",
+ "officialFixCommit": "e47e56d226519635768e6aab2f38f0ab037c09e5",
+ "officialReleaseCommit": "34f7186b86743a083a589741b6cea95293524108",
+ "officialCompareURL": "https://api.github.com/repos/jqlang/jq/compare/e47e56d226519635768e6aab2f38f0ab037c09e5...34f7186b86743a083a589741b6cea95293524108?per_page=1",
+ "status": "ahead",
+ "mergeBase": "e47e56d226519635768e6aab2f38f0ab037c09e5",
+ "evidenceSha256": "129bd92e68e62a2ad8fadcce03f5676d5f997105d2d71c1f5ea40ca83984614e",
+ "reason": "Official security-fix commit is an ancestor of the pinned release; exact release source hashes are verified before packaging. Existing patch file is preserved; only its series entry is removed.",
+ "reverseCheck": "Context/later security-code differences; inclusion established by official ancestry and pinned release source."
+ },
+ {
+ "patch": "CVE-2026-33947.patch",
+ "originalPatchSha256": "5ac5af1290625a4e8c3ee609562c2d2569fcdf9f9cba39b0b130983cfb28b572",
+ "officialFixCommit": "fb59f1491058d58bdc3e8dd28f1773d1ac690a1f",
+ "officialReleaseCommit": "34f7186b86743a083a589741b6cea95293524108",
+ "officialCompareURL": "https://api.github.com/repos/jqlang/jq/compare/fb59f1491058d58bdc3e8dd28f1773d1ac690a1f...34f7186b86743a083a589741b6cea95293524108?per_page=1",
+ "status": "ahead",
+ "mergeBase": "fb59f1491058d58bdc3e8dd28f1773d1ac690a1f",
+ "evidenceSha256": "d4d669acc6b567e71809ef57390b8ae8c75478d1177cebafec5bd3e8381bd881",
+ "reason": "Official security-fix commit is an ancestor of the pinned release; exact release source hashes are verified before packaging. Existing patch file is preserved; only its series entry is removed.",
+ "reverseCheck": "Context/later security-code differences; inclusion established by official ancestry and pinned release source."
+ },
+ {
+ "patch": "CVE-2026-33948.patch",
+ "originalPatchSha256": "dfed461e2779f12c5feb80cbe39a96f937337a66416f9c1cfa7b271edafdce1f",
+ "officialFixCommit": "6374ae0bcdfe33a18eb0ae6db28493b1f34a0a5b",
+ "officialReleaseCommit": "34f7186b86743a083a589741b6cea95293524108",
+ "officialCompareURL": "https://api.github.com/repos/jqlang/jq/compare/6374ae0bcdfe33a18eb0ae6db28493b1f34a0a5b...34f7186b86743a083a589741b6cea95293524108?per_page=1",
+ "status": "ahead",
+ "mergeBase": "6374ae0bcdfe33a18eb0ae6db28493b1f34a0a5b",
+ "evidenceSha256": "2e81e6b8068d92bb7c6c46ff2dc7b26fff517df61fd9dd99fadd3ed31ddcb8a8",
+ "reason": "Official security-fix commit is an ancestor of the pinned release; exact release source hashes are verified before packaging. Existing patch file is preserved; only its series entry is removed.",
+ "reverseCheck": "Context/later security-code differences; inclusion established by official ancestry and pinned release source."
+ },
+ {
+ "patch": "CVE-2026-39956.patch",
+ "originalPatchSha256": "b3403757544145d2c13f59a517f7db7aa78948a492540e5fdf7e220fd4ed2d69",
+ "officialFixCommit": "fdf8ef0f0810e3d365cdd5160de43db46f57ed03",
+ "officialReleaseCommit": "34f7186b86743a083a589741b6cea95293524108",
+ "officialCompareURL": "https://api.github.com/repos/jqlang/jq/compare/fdf8ef0f0810e3d365cdd5160de43db46f57ed03...34f7186b86743a083a589741b6cea95293524108?per_page=1",
+ "status": "ahead",
+ "mergeBase": "fdf8ef0f0810e3d365cdd5160de43db46f57ed03",
+ "evidenceSha256": "ac96a4c2a113e691fb7356220f3c007517dd52a525c834cb8fe82fb893d39ca7",
+ "reason": "Official security-fix commit is an ancestor of the pinned release; exact release source hashes are verified before packaging. Existing patch file is preserved; only its series entry is removed.",
+ "reverseCheck": "PASS"
+ },
+ {
+ "patch": "CVE-2026-39979.patch",
+ "originalPatchSha256": "b97d5778c58cdd80673d2df1bf0b822e606c304d091e3433358b3a9d4ca34a86",
+ "officialFixCommit": "2f09060afab23fe9390cce7cb860b10416e1bf5f",
+ "officialReleaseCommit": "34f7186b86743a083a589741b6cea95293524108",
+ "officialCompareURL": "https://api.github.com/repos/jqlang/jq/compare/2f09060afab23fe9390cce7cb860b10416e1bf5f...34f7186b86743a083a589741b6cea95293524108?per_page=1",
+ "status": "ahead",
+ "mergeBase": "2f09060afab23fe9390cce7cb860b10416e1bf5f",
+ "evidenceSha256": "d233f73e23140b4c4195afbe6aa6e2a42deabf87fd5736769eda7599c10d67f0",
+ "reason": "Official security-fix commit is an ancestor of the pinned release; exact release source hashes are verified before packaging. Existing patch file is preserved; only its series entry is removed.",
+ "reverseCheck": "PASS"
+ },
+ {
+ "patch": "CVE-2026-40164.patch",
+ "originalPatchSha256": "91ff96bae1b5d8b83b76db5ff2d80ec1a7ad6245a5d1cae6487eaffafd0a1440",
+ "officialFixCommit": "0c7d133c3c7e37c00b6d46b658a02244fdd3c784",
+ "officialReleaseCommit": "34f7186b86743a083a589741b6cea95293524108",
+ "officialCompareURL": "https://api.github.com/repos/jqlang/jq/compare/0c7d133c3c7e37c00b6d46b658a02244fdd3c784...34f7186b86743a083a589741b6cea95293524108?per_page=1",
+ "status": "ahead",
+ "mergeBase": "0c7d133c3c7e37c00b6d46b658a02244fdd3c784",
+ "evidenceSha256": "e8eb1df9b9112de26ddf445110b2c3a1f686fa0cdcfeda94f2b2d5ed517ebc39",
+ "reason": "Official security-fix commit is an ancestor of the pinned release; exact release source hashes are verified before packaging. Existing patch file is preserved; only its series entry is removed.",
+ "reverseCheck": "PASS"
+ }
+ ],
+ "releaseSourceSha256": {
+ "src/jv.c": "a40433614bcf7e344a4256c4aaab7acd4ff3fd09b04e3caefd4e3eb102cbacc2",
+ "src/jv_aux.c": "fb7a4fff8f89ba06e8b9bab4c8047e5f118d971d790060388e028811f6d42cb2",
+ "src/util.c": "1f413a1ffeb9194f246f7d9079169d41126686ca212d7b262a1d7b8cf783be06",
+ "src/builtin.c": "0a29617dbf74a6fdabf644f5da82269113fef69bb46efaa3b232515d90305342",
+ "src/jv_parse.c": "3ba36daee76f99543761f1fe6a6f1d261e6ecfecb8578136cd32c76d093ff40b",
+ "configure.ac": "5bf3b41f9bee589f30db0f32203ada104fd62d278c2e95458fff7f9eb7322fa6",
+ "Makefile.am": "77edc24562d1fff62d87a701c2c287444faf327696a52ffc3bcdc9ea9f00ea0c",
+ "src/execute.c": "8069c06b05f5220a705977f612ae79557e272fba22bd4fc92e69e35a04ad5a16",
+ "src/exec_stack.h": "08092864ce27c09ceb3788813a3d9c056aa9130eb513558e30865356a8d7c9a8",
+ "src/linker.c": "07592a2aca011fa0ec6c0be9ea2cb280e74cc9db325c15a3688674828bc25c63"
+ },
+ "ubuntuPackagingSha256": {
+ "debian/rules": "7434f091fc61bc9c9dba647f92230606ba59330b5263a4059a4281f319e5f68b",
+ "debian/control": "8462edd4f555a10a6891924ca868a8571d7e3148411085ac4cd9622c0b9dfc29",
+ "debian/libjq1.symbols": "5c2db775e6fee872d846857bfbac4e0ed7a8ea90cb1ed05783004d02302053fd",
+ "debian/copyright": "92965c1a25cfced63db0fcc869d060d773d6105da9bd8d3da232ed1691cda778"
+ },
+ "postUbuntuPatchSha256": {
+ "Makefile.am": "08c90cb1bb74f2ea981dbe96bd061102bcaf739167e64805996aee6ecde160b3",
+ "configure.ac": "a818b6f5709a222be56a220c3be373866b1eafc08fb2cfc3cc1e1eb0c2e08a0c",
+ "scripts/version": "3f879889a892093320b25c0da50b37193fe98c3e9a9f312bea1fc51691b80548"
+ },
+ "owningTests": [
+ "mantest",
+ "jqtest",
+ "shtest",
+ "utf8test",
+ "base64test",
+ "uritest",
+ "optionaltest",
+ "onigtest",
+ "manonigtest"
+ ],
+ "sharedHelpers": {
+ "module": "../build-pcre2-backport.py",
+ "functions": [
+ "require",
+ "digest",
+ "download",
+ "run",
+ "capture",
+ "packaging_snapshot",
+ "require_separate_paths"
+ ]
+ },
+ "buildContract": {
+ "nativeOnly": true,
+ "buildUser": "non-root",
+ "originalConfigure": [
+ "--disable-static"
+ ],
+ "preserveOriginalHardening": "hardening=+all abi=+time64 from original debian/rules",
+ "soname": "libjq.so.1",
+ "onigurumaSoname": "libonig.so.5",
+ "newTestSkips": 0,
+ "installRuntime": false
+ },
+ "findingCounts": {
+ "originalPendingCves": [
+ "CVE-2026-41256",
+ "CVE-2026-41257",
+ "CVE-2026-43895",
+ "CVE-2026-43896",
+ "CVE-2026-44777"
+ ],
+ "newOfficialCvesAwaitingProductConfirmation": [
+ "CVE-2026-40612",
+ "CVE-2026-43894",
+ "CVE-2026-47770",
+ "CVE-2026-49839",
+ "CVE-2026-54679"
+ ],
+ "doNotChangeOriginal53Baseline": true
+ },
+ "limitations": [
+ "Official source bytes are SHA-256 pinned over HTTPS; the local preparation environment lacks the uploader PGP key, so it does not claim verified Ubuntu inline signatures or Canonical-issued custom outputs.",
+ "Source preparation passed; native package build, full owning tests, ABI/package and extracted-package smoke checks are required before a product remediation claim.",
+ "Three old Ubuntu patch reverse checks pass; the other three differ in context or later security implementations. All six official fix commits were independently confirmed as ancestors of the pinned release; source hashes are pinned rather than bypassing failed checks.",
+ "Additional official disclosures are recorded separately from the original53 findings; current artifact reachability has not been established for every disclosure."
+ ]
+}
diff --git a/server/security/ubuntu-backports/jq/test_jq_release.py b/server/security/ubuntu-backports/jq/test_jq_release.py
new file mode 100644
index 0000000000..7512bb0c90
--- /dev/null
+++ b/server/security/ubuntu-backports/jq/test_jq_release.py
@@ -0,0 +1,117 @@
+"""Network-free guards for the official stable source/Ubuntu packaging boundary."""
+import contextlib
+import copy
+import importlib.util
+import io
+import json
+from pathlib import Path
+import sys
+import tarfile
+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('jq_release', HERE / 'build-jq-release.py')
+M = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(M)
+
+
+class JqStable(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix='jq-source-contract-')
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name)
+ self.lock = M.load_lock()
+
+ def read_altered(self, field, value):
+ lock = copy.deepcopy(self.lock)
+ lock[field] = value
+ file = self.root / 'fixture.json'
+ file.write_text(json.dumps(lock), encoding='utf-8')
+ with mock.patch.object(M, 'LOCK', file):
+ return M.load_lock()
+
+ def test_official_release_and_complete_security_inventory(self):
+ self.assertEqual('71b8d6e8f5fe81f6c6d0d110e3892251f6ce76ed095abd315e26e6e1193af3af',
+ self.lock['upstream']['sha256'])
+ self.assertEqual(16, len(self.lock['upstream']['releaseSecurityCves']))
+ self.assertEqual(6, len(self.lock['removedUbuntuPatches']))
+ self.assertEqual(4, len(self.lock['retainedUbuntuPatches']))
+
+ def test_other_versions_architectures_packages_and_incomplete_coverage_fail(self):
+ for key, bad in [('upstreamVersion', '1.8.1'), ('architecture', 'arm64'),
+ ('runtimePackages', ['jq']), ('owningTests', ['jqtest'])]:
+ with self.subTest(key=key):
+ with self.assertRaises(ValueError): self.read_altered(key, bad)
+
+ def test_foreign_url_or_unpinned_release_fails(self):
+ for key, bad in [('url', 'https://example.invalid/jq.tar.gz'), ('sha256', ''),
+ ('name', '../outside.tar.gz')]:
+ upstream = copy.deepcopy(self.lock['upstream'])
+ upstream[key] = bad
+ with self.assertRaises(ValueError): self.read_altered('upstream', upstream)
+
+ def test_removal_requires_exact_original_fix_ancestry(self):
+ removed = copy.deepcopy(self.lock['removedUbuntuPatches'])
+ removed[0]['mergeBase'] = '0' * 40
+ with self.assertRaises(ValueError): self.read_altered('removedUbuntuPatches', removed)
+
+ def test_tampered_cached_source_is_not_refetched(self):
+ inputs, cache = self.root / 'inputs', self.root / 'cache'
+ inputs.mkdir(); cache.mkdir()
+ item = self.lock['upstream']
+ (cache / item['name']).write_bytes(b'tampered')
+ with mock.patch.object(M.C.urllib.request, 'build_opener') as opener:
+ with self.assertRaises(ValueError): M.download(item, inputs, cache)
+ opener.assert_not_called()
+
+ def test_archive_cannot_extract_outside_pinned_source_root(self):
+ archive = self.root / 'bad.tar.gz'
+ with tarfile.open(archive, 'w:gz') as source:
+ info = tarfile.TarInfo('../outside')
+ info.size = 1
+ source.addfile(info, io.BytesIO(b'x'))
+ with self.assertRaises(ValueError): M.safe_unpack(archive, self.root)
+ self.assertFalse((self.root.parent / 'outside').exists())
+
+ def test_all_nine_owning_suites_must_pass_without_new_skip(self):
+ for name in M.TESTS:
+ (self.root / (name + '.trs')).write_text(':test-result: PASS\n', encoding='utf-8')
+ self.assertEqual(9, len(M.test_receipts(self.root)))
+ (self.root / 'onigtest.trs').write_text(':test-result: SKIP\n', encoding='utf-8')
+ with self.assertRaises(ValueError): M.test_receipts(self.root)
+ (self.root / 'onigtest.trs').unlink()
+ with self.assertRaises(ValueError): M.test_receipts(self.root)
+
+ def test_original_packaging_cannot_change_silently(self):
+ file = self.root / 'rules'
+ file.write_bytes(b'original')
+ hashes = {'rules': M.digest(file)}
+ M.verify_hashes(self.root, hashes)
+ file.write_bytes(b'changed')
+ with self.assertRaises(ValueError): M.verify_hashes(self.root, hashes)
+
+ def test_root_cannot_run_the_build_or_full_suite(self):
+ with mock.patch.object(Path, 'read_text', return_value='ID=ubuntu\nVERSION_ID="26.04"\n'), \
+ mock.patch.object(M.os, 'geteuid', create=True, return_value=0):
+ with self.assertRaisesRegex(ValueError, 'non-root'):
+ M.build(None, self.lock)
+
+ def test_cli_cannot_select_a_lock_or_arbitrary_jobs(self):
+ for args in [['--lock', '/tmp/untrusted'], ['--jobs', '8']]:
+ with mock.patch.object(sys, 'argv', ['build-jq-release.py'] + args), \
+ contextlib.redirect_stderr(io.StringIO()):
+ with self.assertRaises(SystemExit) as error: M.main()
+ self.assertEqual(2, error.exception.code)
+
+ def test_source_field_may_only_be_omitted_for_same_name_binary(self):
+ M.verify_source_identity('jq', '')
+ M.verify_source_identity('libjq1', 'jq')
+ for package, value in [('libjq1', ''), ('jq', 'foreign'), ('libjq1', 'jq (1.8.1)')]:
+ with self.assertRaises(ValueError): M.verify_source_identity(package, value)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/server/security/ubuntu-backports/libxrender/build-libxrender-backport.py b/server/security/ubuntu-backports/libxrender/build-libxrender-backport.py
new file mode 100644
index 0000000000..b9f917c4f6
--- /dev/null
+++ b/server/security/ubuntu-backports/libxrender/build-libxrender-backport.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+"""Exact libXrender security backport; preserve Ubuntu packaging and public ABI."""
+import importlib.util
+import json
+from pathlib import Path
+import re
+import shlex
+import subprocess
+import sys
+
+HERE = Path(__file__).resolve().parent
+SPEC = importlib.util.spec_from_file_location('source_helpers', HERE.parent / 'source_helpers.py')
+H = importlib.util.module_from_spec(SPEC)
+sys.dont_write_bytecode = True
+SPEC.loader.exec_module(H)
+COMMITS = ['0564bce5446550c1b1681f3d8a933c20ea1455f7']
+
+
+def exports(file, work, env):
+ return {line.split()[-1].split('@')[0] for line in
+ H.capture(['nm', '--dynamic', '--defined-only', str(file)], work, env).splitlines()
+ if line.split()}
+
+
+def check_result(code, output, negative):
+ if negative:
+ H.require(code == 2 and 'malformed header reached allocation:' in output,
+ 'Original-source negative control did not detect the specific vulnerable path')
+ else:
+ H.require(code == 0 and 'PASS' in output, 'Patched/packaged reply-boundary fixture failed')
+
+
+def verify_hardening(dynamic, headers, original_dynamic):
+ def bind_now(text):
+ return 'BIND_NOW' in text or bool(re.search(r'FLAGS_1.*NOW', text))
+
+ # The pinned Ubuntu library has RELRO but no BIND_NOW. Preserve its actual
+ # baseline; do not add build flags or weaken a baseline that does use NOW.
+ H.require('GNU_RELRO' in headers and (not bind_now(original_dynamic) or bind_now(dynamic)),
+ 'Original Ubuntu hardening regressed')
+ return {'RELRO': True, 'BIND_NOW': bind_now(dynamic),
+ 'originalBIND_NOW': bind_now(original_dynamic)}
+
+
+def regression(source, extracted, work, env, log):
+ archive = source / 'build/src/.libs/libXrender.a'
+ H.require(archive.is_file(), 'Original configured static test archive missing')
+ cc = ['cc']
+ for name in ['CPPFLAGS', 'CFLAGS']:
+ cc += shlex.split(H.capture(['dpkg-buildflags', '--get', name], work, env))
+ includes = ['-I' + str(source / x) for x in ['build', 'src', 'include', 'include/X11/extensions']]
+ links = ['-lX11', '-Wl,--wrap=_XReply', '-Wl,--wrap=calloc']
+ fixture = str(HERE / 'reply-boundary.c')
+ old_object = work / 'original-Xrender.o'
+ H.run(cc + includes + ['-DHAVE_CONFIG_H', '-c', str(work / 'original-Xrender.c'),
+ '-o', str(old_object)], work, env, log)
+ executables = []
+ for name, objects in [('patched', [str(archive)]), ('original', [str(old_object), str(archive)]),
+ ('packaged', [str(extracted / 'usr/lib/x86_64-linux-gnu/libXrender.so.1')])]:
+ file = work / ('reply-' + name)
+ H.run(cc + includes + [fixture] + objects + links + ['-o', str(file)], work, env, log)
+ executables.append((name, file))
+ child = dict(env, LD_LIBRARY_PATH=str(extracted / 'usr/lib/x86_64-linux-gnu'))
+ results = []
+ for name, file in executables:
+ command = ['xvfb-run', '--auto-servernum', '--server-args=-nolisten tcp -screen 0 640x480x24',
+ str(file)] + (['--valid-only'] if name == 'packaged' else [])
+ result = subprocess.run(command, cwd=work, env=child, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, text=True, timeout=30)
+ check_result(result.returncode, result.stdout, name == 'original')
+ results.append({'test': name, 'exitCode': result.returncode, 'output': result.stdout,
+ 'status': 'EXPECTED_FAILURE' if name == 'original' else 'PASS'})
+ report = work / 'reply-boundary.json'
+ report.write_text(json.dumps(results, indent=2) + '\n', encoding='utf-8')
+ return results, [(report, report.name)]
+
+
+def verify(lock, source, extracted, work, env, log):
+ # This upstream has no tests: preserve its normal check target, then require
+ # the additional native negative/positive boundary fixtures instead of claiming
+ # zero upstream cases proved the vulnerability fixed.
+ check_log = work / 'owning-check.log'
+ H.run(['make', '-C', 'build', 'check'], source, env, check_log)
+ H.require(not list(source.rglob('*.trs')), 'Unexpected upstream test manifest; reassess full suite')
+ baseline_dir = work / 'original-package'
+ baseline = work / 'inputs' / lock['baselineBinary']['name']
+ for field, expected in [('Package', 'libxrender1'), ('Version', '1:0.9.12-1build1'),
+ ('Architecture', 'amd64')]:
+ H.require(H.capture(['dpkg-deb', '--field', str(baseline), field], work, env) == expected,
+ 'Original Ubuntu ABI baseline identity mismatch')
+ H.run(['dpkg-deb', '--extract', str(baseline), str(baseline_dir)], work, env, log)
+ library = extracted / 'usr/lib/x86_64-linux-gnu/libXrender.so.1'
+ old = baseline_dir / 'usr/lib/x86_64-linux-gnu/libXrender.so.1'
+ H.require(exports(library, work, env) == exports(old, work, env),
+ 'Original Ubuntu exported ABI changed')
+ dynamic = H.capture(['readelf', '--dynamic', str(library)], work, env)
+ old_dynamic = H.capture(['readelf', '--dynamic', str(old)], work, env)
+ needed = lambda text: set(re.findall(r'NEEDED.*\[(.+?)\]', text))
+ H.require('[libXrender.so.1]' in dynamic and needed(dynamic) == needed(old_dynamic),
+ 'SONAME/original runtime dependencies changed')
+ # The original source has no Debian symbols file; compare the exact original
+ # Ubuntu binary exports and NEEDED entries instead.
+ headers = H.capture(['readelf', '--program-headers', str(library)], work, env)
+ hardening = verify_hardening(dynamic, headers, old_dynamic)
+ cases, evidence = regression(source, extracted, work, env, log)
+ return {'owningCheck': 'PASS', 'upstreamDefinedTestCases': 0,
+ 'originalUbuntuAbiAndNeeded': 'PASS', 'originalBinarySha256': lock['baselineBinary']['sha256'],
+ 'hardening': hardening,
+ 'replyBoundaryCases': cases, 'runtimeInstalled': False}, \
+ [(check_log, check_log.name)] + evidence
+
+
+if __name__ == '__main__':
+ try:
+ H.entry(HERE, 'libxrender', '1:0.9.12-1build1', COMMITS, verify,
+ ['cc', 'dpkg-buildflags', 'xvfb-run', 'Xvfb', 'xauth'])
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
+ print('LIBXRENDER_BACKPORT_FAILED: ' + str(error), file=sys.stderr)
+ sys.exit(1)
diff --git a/server/security/ubuntu-backports/libxrender/libxrender.lock.json b/server/security/ubuntu-backports/libxrender/libxrender.lock.json
new file mode 100644
index 0000000000..f438120bcf
--- /dev/null
+++ b/server/security/ubuntu-backports/libxrender/libxrender.lock.json
@@ -0,0 +1,99 @@
+{
+ "schemaVersion": 1,
+ "baselineBinary": {
+ "name": "libxrender1_0.9.12-1build1_amd64.deb",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/libx/libxrender/libxrender1_0.9.12-1build1_amd64.deb",
+ "sha256": "60b2f01e8124318e2a11049b82c764fd4c05c21adba11312e93ab116d6594f1e",
+ "size": 19204,
+ "version": "1:0.9.12-1build1",
+ "indexURL": "https://archive.ubuntu.com/ubuntu/dists/resolute/main/binary-amd64/Packages.xz",
+ "indexSha256": "ed9ac41cb263efaecc5a06a0742dc56570e2fb5ff94c4f2e4b2fbdcddd73cf24",
+ "asOf": "2026-10-24T17:37:59.403844+00:00"
+ },
+ "sourcePackage": "libxrender",
+ "ubuntuSourceVersion": "1:0.9.12-1build1",
+ "customVersion": "1:0.9.12-1build1+pasturestack.1",
+ "distribution": "resolute",
+ "osVersion": "26.04",
+ "architecture": "amd64",
+ "sourceDateEpoch": 1792857600,
+ "identity": "First-party rebuild with unchanged Ubuntu packaging and exact upstream security commits; not issued by Canonical.",
+ "runtimePackages": [
+ "libxrender1"
+ ],
+ "sourceFiles": [
+ {
+ "name": "libxrender_0.9.12-1build1.dsc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/libx/libxrender/libxrender_0.9.12-1build1.dsc",
+ "size": 2282,
+ "sha256": "84d74d1d04750e0742521f453635b0b092b5fb13cdf3aa00c1360c6db2d68e42"
+ },
+ {
+ "name": "libxrender_0.9.12-1build1.diff.gz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/libx/libxrender/libxrender_0.9.12-1build1.diff.gz",
+ "size": 21468,
+ "sha256": "473c174d5df6c9378e52d092909acf42bdbd453af419893df6414294619649ac"
+ },
+ {
+ "name": "libxrender_0.9.12.orig.tar.gz.asc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/libx/libxrender/libxrender_0.9.12.orig.tar.gz.asc",
+ "size": 833,
+ "sha256": "0bbd310ac3974ef398cf4d8a4b362b0b4d60ceb43e6eba393c3cc740b03816fc"
+ },
+ {
+ "name": "libxrender_0.9.12.orig.tar.gz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/libx/libxrender/libxrender_0.9.12.orig.tar.gz",
+ "size": 450034,
+ "sha256": "0fff64125819c02d1102b6236f3d7d861a07b5216d8eea336c3811d31494ecf7"
+ }
+ ],
+ "patches": [
+ {
+ "name": "0564bce5446550c1b1681f3d8a933c20ea1455f7.patch",
+ "url": "https://gitlab.freedesktop.org/xorg/lib/libxrender/-/commit/0564bce5446550c1b1681f3d8a933c20ea1455f7.patch",
+ "size": 1300,
+ "sha256": "565e816e88ca223cebc6f833cfcfa63d20aaa22c8293d878bdd059910c4a66fc",
+ "commit": "0564bce5446550c1b1681f3d8a933c20ea1455f7",
+ "fixedCves": [
+ "CVE-2026-88807"
+ ],
+ "firstFixedVersion": "0.9.13"
+ }
+ ],
+ "baselineSha256": {
+ "src/Xrender.c": "dcaa126454543938604573f5cde44978dbdec7e63e944d1bbf9dcd0ccf249265"
+ },
+ "patchedSha256": {
+ "src/Xrender.c": "c5c93efd29a9e13cabe4f6fe99863c13a14d6c006e90267f747e9da9a8b800c8"
+ },
+ "ubuntuPackagingSha256": {
+ "debian/control": "a0e3839d87703b5529b33c1afb9b51b66f2e43d446b0ee32261e61a1894bd2b4",
+ "debian/copyright": "ed2a78123a5076b4f11da38cb1ffb85b5cb9fdc26b5746c517b1d7e7a9029704",
+ "debian/libxrender-dev.install": "89e5d7d592067025e7a01f3a021e706d5fbd38d36287688f4581165b0386fa3c",
+ "debian/libxrender1-udeb.install": "260da073f134f45afdbb30624dbdbc3cd073a6686ff320a389ebe04b17841c00",
+ "debian/libxrender1.install": "a64e0b5d374cfb996d816612d84d39eb9f30af7bee549edcc71cb016e9383442",
+ "debian/not-installed": "124eb62e79e117cb874319b2c6bbc43f564bdc69ca9a9965c8799d4c26654c93",
+ "debian/README.source": "465394b01938cfeb0907b774eebb8de260196ecbaded5197dfdf80efbe069cf3",
+ "debian/rules": "d42e7ede44c920737206230a9f18930204abaf6aa5af74e8880b2b744d332ba8",
+ "debian/watch": "73b20911768168b7a4959eafdd8b11729dc03bfedee9aa98ff80d373d63b8595",
+ "debian/upstream/signing-key.asc": "41bfb9ac51e4e61957b7f5659470fa211b907069cc613a1f6327e639baef7af7"
+ },
+ "buildContract": {
+ "nativeUbuntuAmd64": true,
+ "buildUser": "non-root",
+ "originalRulesAndDependenciesPreserved": true,
+ "addedSkips": 0,
+ "runtimeInstalled": false,
+ "requiredOwningSuite": "Original upstream defines zero tests; native make check must succeed, plus exact source negative-control and patched/packaged XRender reply-boundary fixtures.",
+ "requiredSoname": "libXrender.so.1",
+ "extraCiOnlyDependencies": [
+ "xvfb",
+ "xauth"
+ ]
+ },
+ "limitations": [
+ "Official patch ships no regression test; an additional bounded Xvfb reply-header fixture checks the actual function against original and patched source.",
+ "Source preparation passed; no native compilation, full suite or packaged binary verification has run locally.",
+ "Source archives/signatures are SHA256-pinned from official Ubuntu HTTPS; this does not claim verified uploader PGP signatures or Canonical-signed custom binaries."
+ ]
+}
diff --git a/server/security/ubuntu-backports/libxrender/reply-boundary.c b/server/security/ubuntu-backports/libxrender/reply-boundary.c
new file mode 100644
index 0000000000..59029822fe
--- /dev/null
+++ b/server/security/ubuntu-backports/libxrender/reply-boundary.c
@@ -0,0 +1,70 @@
+/* Exercise the actual XRenderQueryFormats reply boundary against an isolated Xvfb.
+ * The original source safely reaches allocation after the malformed header;
+ * patched source must reject it before allocating. No malformed payload is sent.
+ */
+#include
+#include
+#include
+#include
+#include
+#include
+
+static int armed, injected, after_reply, allocations;
+extern Status __real__XReply(Display *, xReply *, int, Bool);
+extern void *__real_calloc(size_t, size_t);
+
+void *__wrap_calloc(size_t n, size_t size)
+{
+ if (after_reply)
+ allocations++;
+ return __real_calloc(n, size);
+}
+
+Status __wrap__XReply(Display *display, xReply *reply, int extra, Bool discard)
+{
+ Status result = __real__XReply(display, reply, extra, discard);
+ if (armed && result) {
+ xRenderQueryPictFormatsReply *formats = (void *) reply;
+ if (formats->numScreens == 0 || formats->numScreens > 100 ||
+ formats->numSubpixel != formats->numScreens) {
+ fprintf(stderr, "unexpected isolated Xvfb reply shape\n");
+ exit(3);
+ }
+ formats->numSubpixel = formats->numScreens + 1;
+ injected++;
+ after_reply = 1;
+ }
+ return result;
+}
+
+int main(int argc, char **argv)
+{
+ Display *display = XOpenDisplay(NULL);
+ if (!display || !XRenderQueryFormats(display) ||
+ !XRenderFindStandardFormat(display, PictStandardARGB32)) {
+ fprintf(stderr, "valid XRender control failed\n");
+ return 3;
+ }
+ XCloseDisplay(display);
+ if (argc == 2 && strcmp(argv[1], "--valid-only") == 0) {
+ puts("packaged valid XRender control: PASS");
+ return 0;
+ }
+ display = XOpenDisplay(NULL);
+ if (!display)
+ return 3;
+ armed = 1;
+ Status status = XRenderQueryFormats(display);
+ armed = after_reply = 0;
+ XCloseDisplay(display);
+ if (injected != 1 || status != 0) {
+ fprintf(stderr, "malformed reply rejection control failed\n");
+ return 3;
+ }
+ if (allocations != 0) {
+ fprintf(stderr, "malformed header reached allocation: %d\n", allocations);
+ return 2;
+ }
+ puts("valid control and malformed header rejected before allocation: PASS");
+ return 0;
+}
diff --git a/server/security/ubuntu-backports/libxrender/test_libxrender_backport.py b/server/security/ubuntu-backports/libxrender/test_libxrender_backport.py
new file mode 100644
index 0000000000..d1adff6845
--- /dev/null
+++ b/server/security/ubuntu-backports/libxrender/test_libxrender_backport.py
@@ -0,0 +1,96 @@
+"""Network-free guards; native build/test execution belongs to isolated CI."""
+import copy
+import importlib.util
+import json
+from pathlib import Path
+import tempfile
+import unittest
+from unittest import mock
+import sys
+
+sys.dont_write_bytecode = True
+HERE = Path(__file__).resolve().parent
+SPEC = importlib.util.spec_from_file_location('module_backport', HERE / 'build-libxrender-backport.py')
+M = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(M)
+H = M.H
+
+
+class Backport(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory(prefix='official-source-unit-')
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name)
+ self.lock = H.load_lock(HERE / 'libxrender.lock.json', 'libxrender', '1:0.9.12-1build1', M.COMMITS)
+
+ def altered(self, key, value):
+ lock = copy.deepcopy(self.lock)
+ lock[key] = value
+ file = self.root / 'bad.json'
+ file.write_text(json.dumps(lock), encoding='utf-8')
+ return H.load_lock(file, 'libxrender', '1:0.9.12-1build1', M.COMMITS)
+
+ def test_version_architecture_and_commit_boundary_cannot_change(self):
+ for key, value in [('architecture', 'arm64'), ('ubuntuSourceVersion', 'other'),
+ ('patches', []), ('runtimePackages', [])]:
+ with self.assertRaises(ValueError): self.altered(key, value)
+
+ def test_foreign_url_or_unpinned_source_cannot_be_downloaded(self):
+ for key, value in [('url', 'https://example.invalid/source'), ('sha256', ''),
+ ('name', '../outside')]:
+ sources = copy.deepcopy(self.lock['sourceFiles'])
+ sources[0][key] = value
+ with self.assertRaises(ValueError): self.altered('sourceFiles', sources)
+
+ def test_poisoned_cache_fails_closed_without_refetch(self):
+ cache, dest = self.root / 'cache', self.root / 'inputs'
+ cache.mkdir(); dest.mkdir()
+ item = self.lock['sourceFiles'][0]
+ (cache / item['name']).write_bytes(b'poison')
+ with mock.patch.object(H.COMMON.urllib.request, 'build_opener') as opener:
+ with self.assertRaises(ValueError): H.COMMON.download(item, dest, cache)
+ opener.assert_not_called()
+
+ def test_source_and_packaging_hash_mismatch_is_fatal(self):
+ file = self.root / 'input'
+ file.write_bytes(b'original')
+ expected = {'input': H.digest(file)}
+ H.verify_hashes(self.root, expected)
+ file.write_bytes(b'changed')
+ with self.assertRaises(ValueError): H.verify_hashes(self.root, expected)
+
+ def test_root_cannot_run_build_or_owning_tests(self):
+ with mock.patch.object(Path, 'read_text', return_value='ID=ubuntu\nVERSION_ID="26.04"\n'), \
+ mock.patch.object(H.os, 'geteuid', create=True, return_value=0):
+ with self.assertRaisesRegex(ValueError, 'non-root'):
+ H.build(None, self.lock, HERE, M.verify, [])
+
+ def test_old_negative_control_must_fail_for_the_specific_reply_boundary(self):
+ M.check_result(2, 'malformed header reached allocation: 1\n', True)
+ for code, output in [(0, 'PASS'), (2, 'unrelated failure'), (-11, 'crashed')]:
+ with self.assertRaises(ValueError): M.check_result(code, output, True)
+
+ def test_candidate_and_packaged_controls_must_pass(self):
+ M.check_result(0, 'valid control: PASS', False)
+ with self.assertRaises(ValueError): M.check_result(2, 'PASS', False)
+
+ def test_original_ubuntu_abi_binary_is_exactly_scoped(self):
+ baseline = copy.deepcopy(self.lock['baselineBinary'])
+ baseline['version'] = 'other'
+ with self.assertRaises(ValueError): self.altered('baselineBinary', baseline)
+
+ def test_actual_ubuntu_relro_baseline_does_not_require_new_bind_now_flags(self):
+ dynamic = '0x000000000000000e (SONAME) Library soname: [libXrender.so.1]'
+ self.assertEqual({'RELRO': True, 'BIND_NOW': False, 'originalBIND_NOW': False},
+ M.verify_hardening(dynamic, 'GNU_RELRO', dynamic))
+
+ def test_existing_relro_and_bind_now_cannot_regress(self):
+ for original in ['BIND_NOW', 'FLAGS_1 Flags: NOW']:
+ with self.subTest(original=original):
+ with self.assertRaises(ValueError): M.verify_hardening('', 'GNU_RELRO', original)
+ self.assertTrue(M.verify_hardening(original, 'GNU_RELRO', original)['BIND_NOW'])
+ with self.assertRaises(ValueError): M.verify_hardening('', '', '')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/server/security/ubuntu-backports/p11-kit/build-p11-kit-backport.py b/server/security/ubuntu-backports/p11-kit/build-p11-kit-backport.py
new file mode 100644
index 0000000000..9e966b62a9
--- /dev/null
+++ b/server/security/ubuntu-backports/p11-kit/build-p11-kit-backport.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+"""Exact p11-kit RPC security backports, with unchanged Ubuntu owning tests."""
+from collections import Counter
+import ctypes
+import importlib.util
+import json
+from pathlib import Path
+import re
+import subprocess
+import sys
+
+HERE = Path(__file__).resolve().parent
+SPEC = importlib.util.spec_from_file_location('source_helpers', HERE.parent / 'source_helpers.py')
+H = importlib.util.module_from_spec(SPEC)
+sys.dont_write_bytecode = True
+SPEC.loader.exec_module(H)
+COMMITS = ['0eedd4ddd7c924f9cbb4ac496b3fda699435c3cf', '3e64244e538550c6a7fcf826fa8c50a4604416dc']
+EXTRA_TOOLS = ['p11tool']
+
+
+def require_tap_ok(text, case):
+ pattern = (r'^(not ok|ok)[ \t]+[0-9]+[ \t]+(?:-[ \t]+)?' + re.escape(case) +
+ r'((?:[ \t]+.*)?)$')
+ results = [(state, suffix.strip()) for state, suffix in re.findall(pattern, text, re.M)]
+ H.require(results == [('ok', '')],
+ 'Official regression case lacks a single unqualified TAP ok: ' + case)
+
+
+def configured_test_logs(source, env):
+ # Query the original generated Makefile without running a test target.
+ # TEST_LOGS intentionally retains any duplicate entries used by Automake's
+ # summary; p11-kit 0.26.2 lists test-oid through two source fragments.
+ return H.capture(['make', '--no-print-directory', '--eval',
+ "pasturestack-print-test-logs:;@printf '%s\\n' $(TEST_LOGS)",
+ 'pasturestack-print-test-logs'], source, env).splitlines()
+
+
+def full_suite(source, test_logs):
+ logs = list(source.rglob('test-suite.log'))
+ H.require(len(logs) == 1, 'Missing/ambiguous full owning test-suite log')
+ summary = logs[0].read_text(encoding='utf-8', errors='replace')
+ states = ['PASS', 'SKIP', 'XFAIL', 'FAIL', 'XPASS', 'ERROR']
+ pairs = re.findall(r'^# (TOTAL|PASS|SKIP|XFAIL|FAIL|XPASS|ERROR):[ \t]*([0-9]+)[ \t]*$',
+ summary, re.M)
+ H.require(len(pairs) == 7 and {key for key, _ in pairs} == {'TOTAL', *states},
+ 'Missing or ambiguous owning-suite summary counts')
+ counts = {key: int(value) for key, value in pairs}
+ H.require(test_logs and all(not Path(name).is_absolute() and '..' not in Path(name).parts
+ and Path(name).suffix == '.log' for name in test_logs),
+ 'Missing or unsafe configured TEST_LOGS manifest')
+ occurrences = Counter(source / Path(name).with_suffix('.trs') for name in test_logs)
+ reports = list(occurrences)
+ H.require(set(reports) == set(source.rglob('*.trs')),
+ 'Configured TEST_LOGS and native TRS files differ')
+ H.require(reports and counts['TOTAL'] > 0, 'Complete owning suite has no testcase results')
+ receipts, evidence = [], [(logs[0], 'test-suite.log')]
+ names, observed, unique = set(), Counter(), Counter()
+ for report in reports:
+ text = report.read_text(encoding='utf-8')
+ # tap-driver writes one result per TAP testcase, not one per .trs file.
+ result_lines = [line for line in text.splitlines() if line.startswith(':test-result:')]
+ matches = [re.fullmatch(r':test-result: (PASS|SKIP|XFAIL|FAIL|XPASS|ERROR)', line)
+ for line in result_lines]
+ H.require(matches and all(matches), 'Missing or invalid TRS testcase result: ' + str(report))
+ H.require(re.findall(r'^:global-test-result: (\S+)$', text, re.M) == ['PASS'],
+ 'Owning TRS global result did not PASS: ' + str(report))
+ case_counts = Counter(match[1] for match in matches)
+ unique.update(case_counts)
+ observed.update({state: count * occurrences[report] for state, count in case_counts.items()})
+ relative = report.relative_to(source).as_posix()
+ names.add(report.stem)
+ receipts.append({'path': relative, 'status': 'PASS', 'sha256': H.digest(report),
+ 'caseCounts': {state: case_counts[state] for state in states},
+ 'summaryOccurrences': occurrences[report]})
+ for suffix in ['.trs', '.log']:
+ file = report.with_suffix(suffix)
+ H.require(file.is_file(), 'Owning test output missing')
+ evidence.append((file, file.relative_to(source).as_posix().replace('/', '__')))
+ H.require(sum(observed.values()) == counts['TOTAL']
+ and all(observed[state] == counts[state] for state in states),
+ 'TRS testcase totals/states do not match owning-suite summary')
+ H.require(counts['PASS'] == counts['TOTAL'] and all(counts[state] == 0 for state in states[1:]),
+ 'Complete owning suite must PASS without skips')
+ H.require({'test-rpc', 'test-rpc-message'} <= names, 'RPC owning regression suites missing')
+ # Verify the new upstream cases actually ran, rather than only trusting a suite name.
+ rpc = next(p for p in reports if p.stem == 'test-rpc').with_suffix('.log').read_text(encoding='utf-8')
+ message = next(p for p in reports if p.stem == 'test-rpc-message').with_suffix('.log').read_text(encoding='utf-8')
+ require_tap_ok(rpc, '/rpc3/recursion-limit')
+ require_tap_ok(message, '/rpc-message/attribute-recursion-limit')
+ return {'status': 'PASS', 'counts': counts, 'receipts': receipts,
+ 'configuredTestLogs': test_logs, 'uniqueCaseTotal': sum(unique.values()),
+ 'uniqueCaseCounts': {state: unique[state] for state in states},
+ 'officialRpcRecursionCases': 'PASS', 'addedSkips': 0}, evidence
+
+
+def verify(lock, source, extracted, work, env, log):
+ tests, evidence = full_suite(source, configured_test_logs(source, env))
+ library = extracted / 'usr/lib/x86_64-linux-gnu/libp11-kit.so.0'
+ abi = H.verify_abi(library, 'libp11-kit.so.0', source / 'debian/libp11-kit0.symbols', work, env)
+ for name in ['usr/bin/p11-kit', 'usr/libexec/p11-kit/p11-kit-remote',
+ 'usr/libexec/p11-kit/p11-kit-server',
+ 'usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so']:
+ H.require((extracted / name).is_file(), 'Original runtime entry point missing: ' + name)
+ child = dict(env, LD_LIBRARY_PATH=str(library.parent))
+ help_text = H.capture([str(extracted / 'usr/bin/p11-kit'), '--help'], work, child)
+ H.require('list-modules' in help_text, 'Packaged CLI help contract failed')
+ api = ctypes.CDLL(str(library))
+ count, table = ctypes.c_ulong(0), ctypes.c_void_p()
+ api.C_GetInterfaceList.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)]
+ api.C_GetInterfaceList.restype = ctypes.c_ulong
+ api.C_GetFunctionList.argtypes = [ctypes.POINTER(ctypes.c_void_p)]
+ api.C_GetFunctionList.restype = ctypes.c_ulong
+ H.require(api.C_GetInterfaceList(None, ctypes.byref(count)) == 0 and count.value > 0
+ and api.C_GetFunctionList(ctypes.byref(table)) == 0 and table.value,
+ 'Packaged PKCS11 public interface smoke failed')
+ return {'fullOwningSuite': tests, 'abi': abi, 'packagedCliAndPkcs11': 'PASS',
+ 'cve18938ArchitectureCaveat': 'Officially32-bit only; nativeamd64 suite does not claim32-bit trigger coverage.'}, evidence
+
+
+if __name__ == '__main__':
+ try:
+ H.entry(HERE, 'p11-kit', '0.26.2-2', COMMITS, verify, EXTRA_TOOLS)
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
+ print('P11_KIT_BACKPORT_FAILED: ' + str(error), file=sys.stderr)
+ sys.exit(1)
diff --git a/server/security/ubuntu-backports/p11-kit/p11-kit.lock.json b/server/security/ubuntu-backports/p11-kit/p11-kit.lock.json
new file mode 100644
index 0000000000..a8df5e57b0
--- /dev/null
+++ b/server/security/ubuntu-backports/p11-kit/p11-kit.lock.json
@@ -0,0 +1,125 @@
+{
+ "schemaVersion": 1,
+ "sourcePackage": "p11-kit",
+ "ubuntuSourceVersion": "0.26.2-2",
+ "customVersion": "0.26.2-2+pasturestack.1",
+ "distribution": "resolute",
+ "osVersion": "26.04",
+ "architecture": "amd64",
+ "sourceDateEpoch": 1792857600,
+ "identity": "First-party rebuild with unchanged Ubuntu packaging and exact upstream security commits; not issued by Canonical.",
+ "runtimePackages": [
+ "libp11-kit0",
+ "p11-kit",
+ "p11-kit-modules"
+ ],
+ "sourceFiles": [
+ {
+ "name": "p11-kit_0.26.2-2.debian.tar.xz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/p11-kit/p11-kit_0.26.2-2.debian.tar.xz",
+ "size": 24392,
+ "sha256": "1e0be1ac6c3f47a44305855b1c54e22064516a258eef911119c4944d5530b87f"
+ },
+ {
+ "name": "p11-kit_0.26.2.orig.tar.xz.asc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/p11-kit/p11-kit_0.26.2.orig.tar.xz.asc",
+ "size": 228,
+ "sha256": "a2c205bbac7857adc91eb2670304c2ac6649aa9eaf4aab81bf0e46e124f13d8b"
+ },
+ {
+ "name": "p11-kit_0.26.2.orig.tar.xz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/p11-kit/p11-kit_0.26.2.orig.tar.xz",
+ "size": 1069216,
+ "sha256": "09fd9f44da4813a3141e73d5e7cf7008e5660d0405f13d56c15e1da9dcecf828"
+ },
+ {
+ "name": "p11-kit_0.26.2-2.dsc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/p/p11-kit/p11-kit_0.26.2-2.dsc",
+ "size": 2541,
+ "sha256": "066316279c279ca964caa83d92b0f9263914fbbe3c329419e3eec9ffb30540fb"
+ }
+ ],
+ "patches": [
+ {
+ "name": "0eedd4ddd7c924f9cbb4ac496b3fda699435c3cf.patch",
+ "url": "https://github.com/p11-glue/p11-kit/commit/0eedd4ddd7c924f9cbb4ac496b3fda699435c3cf.patch",
+ "size": 11892,
+ "sha256": "24ad6541730f21f81b1afa203c6d85944975de3f3a0c96379d16e9b2a20b9039",
+ "commit": "0eedd4ddd7c924f9cbb4ac496b3fda699435c3cf",
+ "fixedCves": [
+ "CVE-2026-13757"
+ ],
+ "firstFixedVersion": "0.26.3"
+ },
+ {
+ "name": "3e64244e538550c6a7fcf826fa8c50a4604416dc.patch",
+ "url": "https://github.com/p11-glue/p11-kit/commit/3e64244e538550c6a7fcf826fa8c50a4604416dc.patch",
+ "size": 1735,
+ "sha256": "efabd4390f4072c815406a9ca4aea16b7d5c9d797dce23fd9c44d56032f449c2",
+ "commit": "3e64244e538550c6a7fcf826fa8c50a4604416dc",
+ "fixedCves": [
+ "CVE-2026-18938"
+ ],
+ "firstFixedVersion": "0.26.5"
+ }
+ ],
+ "baselineSha256": {
+ "p11-kit/rpc-message.c": "68e35b13c9f639b6440acca346581a802cf0439602124dcc6d52d87b9d92b787",
+ "p11-kit/rpc-message.h": "5df3c3b08b3cebf8b22df8f21d27bbb776fbd42b537c41a22366ea1cb970f1c1",
+ "p11-kit/rpc-server.c": "25948e84d3a56fbdf3692911fa712d699e6129929fe83e7cb23075de8d7117c1",
+ "p11-kit/test-rpc-message.c": "527a89df034cb54d28d726b803a14174bc509ee3be4d7e47c56ae21e4d029a84",
+ "p11-kit/test-rpc.c": "f083d2c89d8db998ecfbf66f9a8c5aba9ed231fa0e65118c0b1851067bb4c51d"
+ },
+ "patchedSha256": {
+ "p11-kit/rpc-message.c": "65d52baf8995f4b6b26fec92f0f432c0896703ab8735bf9c3dd19e0e50b18d56",
+ "p11-kit/rpc-message.h": "a9148098e2ebe719cad5c901556e1dfb81944f6176b2888cf688331e674b73f6",
+ "p11-kit/rpc-server.c": "091825adc750af3c393deb8e2d8e3597aefe1c4ec0904bd0beadf74c639d3659",
+ "p11-kit/test-rpc-message.c": "71196559d8e46c319b8637bb7a1382e3fa0abca0ef468e88876d4ed32cb27197",
+ "p11-kit/test-rpc.c": "9cbb1fbbfc2c16245c9cbdb66fc05e45e970b7e16a0233bf387e29f4843600c6"
+ },
+ "ubuntuPackagingSha256": {
+ "debian/control": "4d5caa78fda0bad43aa13790d7022ef7e0c26bc7dd70bec6499e4b6d4eee09c0",
+ "debian/copyright": "a5611fbad151d1fa0aa484f6c393a5166af33ce9a9783dfb2712d6b87b462b37",
+ "debian/copyright-scan-patterns.yml": "c4a91cd70f48d5b9e091280df864d1722d15f97fed48490f6aa0694d47356a11",
+ "debian/copyright.template": "200b26e9d07a1f125fe3a8e7472b0bd0f8e5ccf0ff558555bc505be355ad420b",
+ "debian/fill.copyright.blanks.yml": "80edcf06e05e215df17860d73c7d5b9438a45f8164a5e9c24acedb36ae5af18d",
+ "debian/fix.scanned.copyright": "4f869c45f5c9e9fe17ddacac5a17bdfb58b02ad52be385192eb25c22d9f3ac5d",
+ "debian/libp11-kit-dev.install": "66b135b1e5d6a739030a49dc83a68dbc392e78c9f4083798dbabd37272bb031f",
+ "debian/libp11-kit0-dbg.links": "5883982da291a9932e7cd4f3c6bb6baaf994dfc52dd52f65e37ad3f5f58ff2fb",
+ "debian/libp11-kit0.examples": "2de4d9b1abb40e5b063c787ca07416737f8b43d6544a8c401bee4475a17d2825",
+ "debian/libp11-kit0.install": "0ff7b540b3e53d1d4d90544bc953d733e180b55ff772fa85ec29e497196454ef",
+ "debian/libp11-kit0.symbols": "b07399e1b7f27673b14b8e81d9f9c304ab72f29e2bc606f6b70a46d8a4fc2173",
+ "debian/not-installed": "f57dab4f50ef9d0e25350c586d53a9314149dce453f85d12bc5c38ca77b9bf98",
+ "debian/p11-kit-doc.install": "aa7c03ac5a004c8ade863ec9c62d9a3d2fca043841bdcd43249571cc794bbebf",
+ "debian/p11-kit-modules.install": "9e4fbdfb8e45f59c015d3ce110997c96290c22c888bd64e5bd54013e2f2ce485",
+ "debian/p11-kit-modules.links": "5ca4efb4a132c5c298f8fbe8f5fa63e72a55f9639c4d909993d688aaec902ee3",
+ "debian/p11-kit.examples": "9a6157743d04919c4acba3d20b457491f55b3506dfd1da411df8db5b91769fe7",
+ "debian/p11-kit.install": "1235b6f608f831ef615b3a49c4bf3d6300a69613b14e920a3cd47faac1d17e3c",
+ "debian/p11-kit.manpages": "ef4eaa2d15ff2e15350f45059c008e7912065259004f63deaaf9c242adda6801",
+ "debian/rules": "7afbb1b146790964b3edc5aba50641d23767e486514a7d9a746f306f99b57fc7",
+ "debian/watch": "1ea494fa571d525f14b50fac95f658f26400b228dbb05421b8bd9366b6dc7506",
+ "debian/patches/36_enable_locale.diff": "bfc402c700dfde13f7d3be6d74274cf080b88c7b2331d704eaefc2c51dadd728",
+ "debian/patches/series": "54ae2d371d09a8575bf3f14264335d7a8cb0a7149255e7b4f73612091d2d057f",
+ "debian/source/format": "1be7080d72e6b566df3e236ce2c55efdfbbb8fa1c972d825e5b672ff8773be1a",
+ "debian/source/lintian-overrides": "1704a18abe610ec865c58a05b3d305753aab8815c3b1524ab4f763758c679e5b",
+ "debian/upstream/metadata": "d1472eae01af6de9d30e86a5e2930862f14ae7cc6d07d7c494b41309934bf6e3",
+ "debian/upstream/signing-key.asc": "c14380726711246f99ed0244a5a937246b80686bf591dbac71c5340734e4c052"
+ },
+ "buildContract": {
+ "nativeUbuntuAmd64": true,
+ "buildUser": "non-root",
+ "originalRulesAndDependenciesPreserved": true,
+ "addedSkips": 0,
+ "runtimeInstalled": false,
+ "requiredOwningSuite": "Every configured upstream Autotools test must PASS, including test-rpc and test-rpc-message.",
+ "requiredSoname": "libp11-kit.so.0",
+ "extraCiOnlyDependencies": [
+ "gnutls-bin"
+ ]
+ },
+ "limitations": [
+ "CVE-2026-18938 is officially exploitable only on32-bit systems; native amd64 build does not establish 32-bit trigger validation. Its official guard is still included.",
+ "Source preparation passed; no native compilation, full suite or packaged binary verification has run locally.",
+ "Source archives/signatures are SHA256-pinned from official Ubuntu HTTPS; this does not claim verified uploader PGP signatures or Canonical-signed custom binaries."
+ ]
+}
diff --git a/server/security/ubuntu-backports/p11-kit/test_p11_kit_backport.py b/server/security/ubuntu-backports/p11-kit/test_p11_kit_backport.py
new file mode 100644
index 0000000000..27800d0130
--- /dev/null
+++ b/server/security/ubuntu-backports/p11-kit/test_p11_kit_backport.py
@@ -0,0 +1,202 @@
+"""Network-free guards; native build/test execution belongs to isolated CI."""
+import copy
+import importlib.util
+import json
+from pathlib import Path
+from types import SimpleNamespace
+import tempfile
+import unittest
+from unittest import mock
+import sys
+
+sys.dont_write_bytecode = True
+HERE = Path(__file__).resolve().parent
+SPEC = importlib.util.spec_from_file_location('module_backport', HERE / 'build-p11-kit-backport.py')
+M = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(M)
+H = M.H
+
+
+class Backport(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory(prefix='official-source-unit-')
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name)
+ self.lock = H.load_lock(HERE / 'p11-kit.lock.json', 'p11-kit', '0.26.2-2', M.COMMITS)
+
+ def altered(self, key, value):
+ lock = copy.deepcopy(self.lock)
+ lock[key] = value
+ file = self.root / 'bad.json'
+ file.write_text(json.dumps(lock), encoding='utf-8')
+ return H.load_lock(file, 'p11-kit', '0.26.2-2', M.COMMITS)
+
+ def test_version_architecture_and_commit_boundary_cannot_change(self):
+ for key, value in [('architecture', 'arm64'), ('ubuntuSourceVersion', 'other'),
+ ('patches', []), ('runtimePackages', [])]:
+ with self.assertRaises(ValueError): self.altered(key, value)
+
+ def test_foreign_url_or_unpinned_source_cannot_be_downloaded(self):
+ for key, value in [('url', 'https://example.invalid/source'), ('sha256', ''),
+ ('name', '../outside')]:
+ sources = copy.deepcopy(self.lock['sourceFiles'])
+ sources[0][key] = value
+ with self.assertRaises(ValueError): self.altered('sourceFiles', sources)
+
+ def test_poisoned_cache_fails_closed_without_refetch(self):
+ cache, dest = self.root / 'cache', self.root / 'inputs'
+ cache.mkdir(); dest.mkdir()
+ item = self.lock['sourceFiles'][0]
+ (cache / item['name']).write_bytes(b'poison')
+ with mock.patch.object(H.COMMON.urllib.request, 'build_opener') as opener:
+ with self.assertRaises(ValueError): H.COMMON.download(item, dest, cache)
+ opener.assert_not_called()
+
+ def test_source_and_packaging_hash_mismatch_is_fatal(self):
+ file = self.root / 'input'
+ file.write_bytes(b'original')
+ expected = {'input': H.digest(file)}
+ H.verify_hashes(self.root, expected)
+ file.write_bytes(b'changed')
+ with self.assertRaises(ValueError): H.verify_hashes(self.root, expected)
+
+ def test_root_cannot_run_build_or_owning_tests(self):
+ with mock.patch.object(Path, 'read_text', return_value='ID=ubuntu\nVERSION_ID="26.04"\n'), \
+ mock.patch.object(H.os, 'geteuid', create=True, return_value=0):
+ with self.assertRaisesRegex(ValueError, 'non-root'):
+ H.build(None, self.lock, HERE, M.verify, [])
+
+ def test_server_access_prerequisite_is_required_before_compilation(self):
+ self.assertIn('gnutls-bin', self.lock['buildContract']['extraCiOnlyDependencies'])
+ args = SimpleNamespace(output_dir=self.root / 'out', work_dir=self.root / 'work',
+ cache_dir=None, jobs=2)
+ with mock.patch.object(Path, 'read_text', return_value='ID=ubuntu\nVERSION_ID="26.04"\n'), \
+ mock.patch.object(H.os, 'geteuid', create=True, return_value=10001), \
+ mock.patch.object(H.shutil, 'which', side_effect=lambda tool, **_: None if tool == 'p11tool' else '/usr/bin/' + tool), \
+ mock.patch.object(H, 'run') as native:
+ with self.assertRaisesRegex(ValueError, 'prerequisite missing: p11tool'):
+ H.build(args, self.lock, HERE, M.verify, M.EXTRA_TOOLS)
+ native.assert_not_called()
+
+ def test_build_reaches_first_download_after_native_preflight(self):
+ args = SimpleNamespace(output_dir=self.root / 'output', work_dir=self.root / 'work',
+ cache_dir=None, jobs=2)
+ with mock.patch.object(Path, 'read_text', return_value='ID=ubuntu\nVERSION_ID="26.04"\n'), \
+ mock.patch.object(H.os, 'geteuid', create=True, return_value=10001), \
+ mock.patch.object(H.shutil, 'which', return_value='/usr/bin/mock-tool'), \
+ mock.patch.object(H, 'capture', return_value='amd64') as capture, \
+ mock.patch.object(H, 'run') as run, \
+ mock.patch.object(H.COMMON, 'download',
+ side_effect=RuntimeError('download boundary reached')) as download:
+ with self.assertRaisesRegex(RuntimeError, 'download boundary reached'):
+ H.build(args, self.lock, HERE, M.verify, [])
+ capture.assert_called_once_with(['dpkg', '--print-architecture'], args.work_dir, mock.ANY)
+ download.assert_called_once_with(self.lock['sourceFiles'][0], args.work_dir / 'inputs', None)
+ run.assert_not_called()
+
+ def suite(self):
+ tests = {'test-rpc': 3, 'test-rpc-message': 2, 'test-control': 1}
+ self.test_logs = [name + '.log' for name in tests]
+ for name, cases in tests.items():
+ (self.root / (name + '.trs')).write_text(':global-test-result: PASS\n' +
+ ':test-result: PASS\n' * cases, encoding='utf-8')
+ (self.root / (name + '.log')).write_text('ok 1 /control\n', encoding='utf-8')
+ (self.root / 'test-rpc.log').write_text(
+ '1..3\nok 1 /rpc3/control\nok 2 /rpc3/recursion-limit\nok 3 /rpc3/another-control\n',
+ encoding='utf-8')
+ (self.root / 'test-rpc-message.log').write_text(
+ '1..2\nok 1 /rpc-message/control\nok 2 /rpc-message/attribute-recursion-limit\n',
+ encoding='utf-8')
+ (self.root / 'test-suite.log').write_text(
+ '# TOTAL: 6\n# PASS: 6\n# SKIP: 0\n# XFAIL: 0\n# FAIL: 0\n# XPASS: 0\n# ERROR: 0\n',
+ encoding='utf-8')
+
+ def test_multi_case_trs_totals_match_full_summary(self):
+ self.suite()
+ result = M.full_suite(self.root, self.test_logs)[0]
+ self.assertEqual('PASS', result['status'])
+ self.assertEqual(6, result['counts']['TOTAL'])
+ self.assertEqual(3, len(result['receipts']))
+ self.assertEqual(6, sum(r['caseCounts']['PASS'] for r in result['receipts']))
+
+ def test_skips_and_incomplete_suite_never_become_pass(self):
+ self.suite()
+ summary = self.root / 'test-suite.log'
+ summary.write_text(summary.read_text().replace('# PASS: 6', '# PASS: 5').replace('# SKIP: 0', '# SKIP: 1'))
+ with self.assertRaises(ValueError): M.full_suite(self.root, self.test_logs)
+ self.suite()
+ (self.root / 'test-control.trs').unlink()
+ with self.assertRaises(ValueError): M.full_suite(self.root, self.test_logs)
+
+ def test_case_total_and_each_summary_state_must_match(self):
+ for old, new in [('# TOTAL: 6', '# TOTAL: 7'), ('# PASS: 6', '# PASS: 5')] + [
+ ('# ' + state + ': 0', '# ' + state + ': 1')
+ for state in ['SKIP', 'XFAIL', 'FAIL', 'XPASS', 'ERROR']]:
+ with self.subTest(summary=new):
+ self.suite()
+ summary = self.root / 'test-suite.log'
+ summary.write_text(summary.read_text().replace(old, new), encoding='utf-8')
+ with self.assertRaises(ValueError): M.full_suite(self.root, self.test_logs)
+
+ def test_nonpass_trs_case_cannot_hide_among_other_pass_cases(self):
+ for state in ['SKIP', 'XFAIL', 'FAIL', 'XPASS', 'ERROR', 'UNKNOWN']:
+ with self.subTest(state=state):
+ self.suite()
+ report = self.root / 'test-rpc.trs'
+ report.write_text(report.read_text().replace(':test-result: PASS',
+ ':test-result: ' + state, 1), encoding='utf-8')
+ if state != 'UNKNOWN':
+ summary = self.root / 'test-suite.log'
+ summary.write_text(summary.read_text().replace('# PASS: 6', '# PASS: 5').replace(
+ '# ' + state + ': 0', '# ' + state + ': 1'), encoding='utf-8')
+ with self.assertRaises(ValueError): M.full_suite(self.root, self.test_logs)
+
+ def test_required_case_needs_single_unqualified_tap_ok(self):
+ for file, case in [('test-rpc.log', '/rpc3/recursion-limit'),
+ ('test-rpc-message.log', '/rpc-message/attribute-recursion-limit')]:
+ for text in [case, '# ' + case, 'not ok 2 ' + case, 'ok 2 ' + case + ' # SKIP unavailable',
+ 'ok 2 ' + case + ' # TODO later', 'ok 2 ' + case + '-other',
+ 'ok 2 ' + case + '\nnot ok 3 ' + case,
+ 'ok 2 ' + case + '\nok 3 ' + case]:
+ with self.subTest(file=file, log=text):
+ self.suite()
+ (self.root / file).write_text(text + '\n', encoding='utf-8')
+ with self.assertRaises(ValueError): M.full_suite(self.root, self.test_logs)
+
+ def test_configured_duplicate_logs_preserve_summary_and_unique_case_counts(self):
+ self.suite()
+ # Automake counts a configured TEST_LOGS entry twice, while the file and
+ # actual testcase execution exist only once. No skip or failure is hidden.
+ manifest = self.test_logs + ['test-rpc-message.log']
+ summary = self.root / 'test-suite.log'
+ summary.write_text(summary.read_text().replace('# TOTAL: 6', '# TOTAL: 8').replace(
+ '# PASS: 6', '# PASS: 8'), encoding='utf-8')
+ result = M.full_suite(self.root, manifest)[0]
+ self.assertEqual(8, result['counts']['TOTAL'])
+ self.assertEqual(6, result['uniqueCaseTotal'])
+ self.assertEqual(2, next(r for r in result['receipts']
+ if r['path'] == 'test-rpc-message.trs')['summaryOccurrences'])
+ with self.assertRaises(ValueError): M.full_suite(self.root, self.test_logs)
+
+ def test_configured_manifest_cannot_omit_or_add_native_trs_files(self):
+ self.suite()
+ for manifest in [self.test_logs[:-1], self.test_logs + ['missing.log'],
+ self.test_logs + ['../outside.log'], []]:
+ with self.subTest(manifest=manifest), self.assertRaises(ValueError):
+ M.full_suite(self.root, manifest)
+
+ def test_test_log_query_preserves_configured_order_and_duplicates(self):
+ with mock.patch.object(H, 'capture', return_value='test-rpc.log\ntest-oid.log\ntest-oid.log') as query:
+ self.assertEqual(['test-rpc.log', 'test-oid.log', 'test-oid.log'],
+ M.configured_test_logs(self.root, {}))
+ command = query.call_args[0][0]
+ self.assertEqual('make', command[0])
+ self.assertIn('$(TEST_LOGS)', command[3])
+ self.assertEqual('pasturestack-print-test-logs', command[-1])
+
+ def test_cve18938_retains_32bit_qualification(self):
+ self.assertTrue(any('32-bit' in value for value in self.lock['limitations']))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/server/security/ubuntu-backports/rust-coreutils/build-rust-coreutils-backport.py b/server/security/ubuntu-backports/rust-coreutils/build-rust-coreutils-backport.py
new file mode 100644
index 0000000000..3effac466c
--- /dev/null
+++ b/server/security/ubuntu-backports/rust-coreutils/build-rust-coreutils-backport.py
@@ -0,0 +1,445 @@
+#!/usr/bin/env python3
+"""Build a first-party rust-coreutils security backport in Ubuntu 26.04 amd64.
+
+Uses the unchanged Ubuntu 0.10 packaging and vendored crates; backports only the
+official id fix. No runtime package installation or alternatives changes occur.
+The original Ubuntu exclusions remain; its accidental positional filter and
+failure swallowing are removed. Extracted-package GID tests remain mandatory.
+"""
+import argparse
+import datetime
+import email.utils
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+import tempfile
+import urllib.request
+
+HERE = Path(__file__).resolve().parent
+LOCK_FILE = HERE / 'rust-coreutils.lock.json'
+VERSION = '0.10.0-1ubuntu2~26.04.1'
+COMMIT = 'ff382f527994b8a021ec0a086dec4c5cafa34f2a'
+PATHS = ['src/uu/id/src/id.rs', 'tests/by-util/test_id.rs']
+RULES_SHA256 = '06c5fd183a4a1146748810e3cce859ba849643eeee244cc80cbb3315aeedb5e7'
+UBUNTU_EXCLUSIONS = ["test_dd::test_stdin_stdout_skip_w_multiplier","test_env::test_env_arg_ignore_signal_valid_signals","test_env::test_env_french","test_env::test_simulation_of_terminal_for_stdin_only","test_env::test_simulation_of_terminal_for_stdout_only","test_env::test_simulation_of_terminal_size_information","test_env::test_simulation_of_terminal_true","test_chmod::test_chmod_colored_output","test_nohup::test_nohup_stderr_to_stdout","test_nohup::test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced","test_tail::test_following_with_pid","test_tail::test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same_size","test_touch::test_touch_changes_time_of_file_in_stdout","test_error_messages_french_translation","test_french_colored_error_messages","test_help_messages_french_translation","util_invalid_name_invalid_command"]
+ID_TESTS = ["test_id::test_id_default_format","test_id::test_id_digital_username","test_id::test_id_groups_ordering","test_id::test_id_ignore","test_id::test_id_multiple_users","test_id::test_id_multiple_users_non_existing","test_id::test_id_name","test_id::test_id_name_or_real_with_default_format","test_id::test_id_no_specified_user","test_id::test_id_no_specified_user_posixly","test_id::test_id_pretty_print_password_record","test_id::test_id_pretty_print_suid_binary","test_id::test_id_real","test_id::test_id_single_user","test_id::test_id_single_user_non_existing","test_id::test_id_zero","test_id::test_id_zero_with_default_format","test_id::test_id_zero_with_name_or_real","test_id::test_invalid_arg"]
+
+
+def require(condition, message):
+ if not condition:
+ raise ValueError(message)
+
+
+def job_option(jobs):
+ if jobs == 2:
+ return '-j2'
+ if jobs == 4:
+ return '-j4'
+ raise ValueError('Build jobs must be 2 or 4')
+
+
+def digest(file):
+ sha = hashlib.sha256()
+ with file.open('rb') as stream:
+ for chunk in iter(lambda: stream.read(1024 * 1024), b''):
+ sha.update(chunk)
+ return sha.hexdigest()
+
+
+def load_lock(file):
+ lock = json.loads(file.read_text(encoding='utf-8'))
+ expected = {'schemaVersion': 1, 'sourcePackage': 'rust-coreutils',
+ 'ubuntuSourceVersion': VERSION, 'customVersion': VERSION + '+pasturestack.1',
+ 'distribution': 'resolute', 'osVersion': '26.04', 'architecture': 'amd64',
+ 'runtimePackages': ['rust-coreutils']}
+ require(all(lock.get(k) == v for k, v in expected.items()), 'Unsupported lock identity')
+ require(type(lock['sourceDateEpoch']) is int and lock['sourceDateEpoch'] > 0,
+ 'Invalid source date')
+ names = {'rust-coreutils_' + VERSION + '.dsc',
+ 'rust-coreutils_0.10.0.orig.tar.gz', 'rust-coreutils_0.10.0.orig-l10n.tar.gz',
+ 'rust-coreutils_' + VERSION + '.debian.tar.xz'}
+ require(len(lock['sourceFiles']) == 4 and
+ {x['name'] for x in lock['sourceFiles']} == names, 'Wrong Ubuntu source set')
+ require(len(lock['patches']) == 1 and lock['patches'][0]['commit'] == COMMIT
+ and lock['patches'][0]['paths'] == PATHS
+ and lock['patches'][0]['fixedCves'] == ['CVE-2026-35370', 'CVE-2026-35371'],
+ 'Only the approved two-CVE patch is supported')
+ for item in lock['sourceFiles'] + lock['patches']:
+ require(re.fullmatch(r'[A-Za-z0-9._~+-]+', item['name']), 'Unsafe input name')
+ require(re.fullmatch(r'[0-9a-f]{64}', item['sha256'])
+ and type(item['size']) is int and 0 < item['size'] < 32 * 1024 * 1024,
+ 'Invalid SHA-256/size')
+ expected_url = ('https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/' +
+ item['name'] if item in lock['sourceFiles'] else
+ 'https://github.com/uutils/coreutils/commit/' + COMMIT + '.patch')
+ require(item['url'] == expected_url, 'Unexpected download URL')
+ require(len(lock['alreadyPresentFixes']) == 18 and
+ len({x['id'] for x in lock['alreadyPresentFixes']}) == 18 and
+ all(x['status'] == 'already_in_original_ubuntu_source'
+ and x['sourcePath'] in lock['baselineSha256']
+ for x in lock['alreadyPresentFixes']), 'Wrong already-present fix inventory')
+ require(set(lock['patchedSha256']) == set(PATHS), 'Unexpected patched paths')
+ for name, sha in {**lock['baselineSha256'], **lock['patchedSha256']}.items():
+ require(not Path(name).is_absolute() and '..' not in Path(name).parts
+ and re.fullmatch(r'[0-9a-f]{64}', sha), 'Invalid source digest entry')
+ return lock
+
+
+class HttpsRedirect(urllib.request.HTTPRedirectHandler):
+ def redirect_request(self, request, file, code, message, headers, new_url):
+ require(new_url.startswith('https://'), 'Refusing HTTPS downgrade')
+ return super().redirect_request(request, file, code, message, headers, new_url)
+
+
+def verify_input(item, file):
+ require(file.is_file() and file.stat().st_size == item['size']
+ and digest(file) == item['sha256'], 'Source identity mismatch: ' + item['name'])
+
+
+def download(item, inputs, cache):
+ file = inputs / item['name']
+ cached = cache / item['name'] if cache else None
+ if cached and cached.exists():
+ verify_input(item, cached) # A poisoned cache fails closed.
+ shutil.copyfile(cached, file)
+ else:
+ with urllib.request.build_opener(HttpsRedirect()).open(item['url'], timeout=60) as response:
+ require(response.url.startswith('https://'), 'Refusing non-HTTPS input')
+ with file.open('xb') as output:
+ size = 0
+ for chunk in iter(lambda: response.read(65536), b''):
+ size += len(chunk)
+ require(size <= item['size'], 'Download exceeds pinned size')
+ output.write(chunk)
+ verify_input(item, file)
+ if cached:
+ shutil.copyfile(file, cached)
+ verify_input(item, file)
+
+
+def verify_source_control(lock, inputs):
+ text = (inputs / ('rust-coreutils_' + VERSION + '.dsc')).read_text(encoding='utf-8')
+ require(all(re.search('^' + re.escape(line) + '$', text, re.M) for line in
+ ['Format: 3.0 (quilt)', 'Source: rust-coreutils', 'Version: ' + VERSION]),
+ 'Unexpected Ubuntu source control')
+ block = re.search(r'^Checksums-Sha256:\n((?: .+\n)+)', text, re.M)
+ require(block, 'No source SHA-256 manifest')
+ actual = {name: (sha, int(size)) for sha, size, name in
+ re.findall(r'^ ([0-9a-f]{64}) (\d+) (\S+)$', block[1], re.M)}
+ require(actual == {x['name']: (x['sha256'], x['size']) for x in lock['sourceFiles']
+ if not x['name'].endswith('.dsc')}, '.dsc/lock mismatch')
+
+
+def verify_patch(item, file):
+ text = file.read_text(encoding='utf-8')
+ require(text.startswith('From ' + COMMIT + ' '), 'Patch commit identity mismatch')
+ paths = re.findall(r'^diff --git a/(\S+) b/(\S+)$', text, re.M)
+ require(paths == [(p, p) for p in PATHS], 'Patch changed unapproved paths')
+
+
+def verify_sources(source, expected):
+ require(all((source / name).is_file() and digest(source / name) == sha
+ for name, sha in expected.items()), 'Unexpected source/packaging bytes')
+
+
+def ubuntu_test_contract(rules):
+ require(digest(rules) == RULES_SHA256, 'Ubuntu owning-test rules digest changed')
+ text = rules.read_text(encoding='utf-8')
+ block = re.search(r'(?ms)^override_dh_auto_test:\n(.*?)^override_dh_auto_install:', text)
+ require(block and 'test||true' in block[1], 'Original Ubuntu test recipe boundary changed')
+ value = re.findall(r'TEST_NO_FAIL_FAST="([^"]+)"', block[1])
+ require(len(value) == 1, 'Ambiguous Ubuntu test arguments')
+ tokens = shlex.split(value[0].replace('\\\n', ''))
+ require(tokens and tokens.pop(0) == '--', 'Unexpected original Cargo test separator')
+ exclusions, positional = [], []
+ while tokens:
+ token = tokens.pop(0)
+ if token == '--skip':
+ require(tokens, 'Missing Ubuntu exclusion pattern')
+ exclusions.append(tokens.pop(0))
+ else:
+ positional.append(token)
+ require(exclusions == UBUNTU_EXCLUSIONS and positional == ['stdout'],
+ 'Original Ubuntu 17 exclusions/legacy positional filter changed')
+ require(not any(pattern in name for pattern in exclusions for name in ID_TESTS),
+ 'Owning test configuration must not exclude an id case')
+ return {'rulesSha256': RULES_SHA256, 'excludedPatterns': exclusions,
+ 'removedLegacyPositionalFilters': positional, 'removedFailureSwallowing': True,
+ 'newExclusionPatterns': []}
+
+
+def ubuntu_test_arguments(contract):
+ require(contract['rulesSha256'] == RULES_SHA256
+ and contract['excludedPatterns'] == UBUNTU_EXCLUSIONS,
+ 'Unapproved owning-test contract')
+ # Build from the approved constants after comparing the parsed pinned recipe.
+ return shlex.join(['--no-fail-fast', '--'] +
+ [value for pattern in UBUNTU_EXCLUSIONS for value in ['--skip', pattern]])
+
+
+def build_environment(lock, jobs, home):
+ # Do not inherit test suppressors, Cargo profiles, compiler flags or registries.
+ environment = {'PATH': '/usr/sbin:/usr/bin:/sbin:/bin', 'HOME': str(home),
+ 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC', 'TERM': 'dumb',
+ 'SOURCE_DATE_EPOCH': str(lock['sourceDateEpoch']),
+ 'DEB_BUILD_OPTIONS': 'parallel=' + str(jobs), 'DEB_BUILD_PROFILES': '',
+ 'DEB_BUILD_MAINT_OPTIONS': '', 'DPKG_COLORS': 'never',
+ 'CARGO_NET_OFFLINE': 'true', 'CARGO_TERM_COLOR': 'never',
+ 'CARGO_BUILD_JOBS': str(jobs)}
+ # Only the real disposable GitHub builder's explicit CI marker is forwarded.
+ # Test filters, compiler overrides and arbitrary CI values are not inherited.
+ if os.environ.get('CI') == 'true':
+ environment['CI'] = 'true'
+ return environment
+
+
+def capture(command, work, env):
+ return subprocess.check_output(command, cwd=work, env=env,
+ text=True, encoding='utf-8').strip()
+
+
+def run(command, work, env, log):
+ print('Running: ' + json.dumps(command), flush=True)
+ with log.open('ab') as output:
+ output.write(('\n$ ' + json.dumps(command) + '\n').encode())
+ output.flush()
+ result = subprocess.run(command, cwd=work, env=env,
+ stdout=output, stderr=subprocess.STDOUT)
+ require(result.returncode == 0, 'Command failed; retained log: ' + str(log))
+
+
+def packaging_snapshot(source):
+ # Original packaging applies its own vendor quilt patches during configure.
+ return {p.relative_to(source).as_posix(): digest(p)
+ for p in sorted((source / 'debian').rglob('*'))
+ if p.is_file() and p.name != 'changelog'
+ and 'rust-vendor' not in p.relative_to(source / 'debian').parts}
+
+
+def strict_test_receipt(log, contract, ci_marker):
+ require(contract['rulesSha256'] == RULES_SHA256
+ and contract['excludedPatterns'] == UBUNTU_EXCLUSIONS, 'Unapproved test contract')
+ text = log.read_text(encoding='utf-8', errors='replace')
+ targets = ['Running unittests src/lib.rs', 'Running unittests src/bin/coreutils.rs',
+ 'Running tests/test_localization_and_colors.rs', 'Running tests/test_util_name.rs',
+ 'Running tests/test_uudoc.rs', 'Running tests/tests.rs', 'Doc-tests coreutils']
+ require(all(target in text for target in targets), 'An owning Cargo target is missing')
+ results = re.findall(r'^test result: (ok|FAILED)\. (\d+) passed; (\d+) failed; (\d+) ignored; '
+ r'(\d+) measured; (\d+) filtered out;', text, re.M)
+ require(len(results) == len(targets) and all(status == 'ok' and int(failed) == 0
+ for status, _, failed, _, _, _ in results) and 'test result: FAILED' not in text,
+ 'Owning-suite failure or incomplete target receipts')
+ ids = re.findall(r'^test (test_id::\S+) \.\.\. (ok|FAILED|ignored)(?:[^\n]*)$', text, re.M)
+ require(len(ids) == len(ID_TESTS) and sorted(name for name, _ in ids) == ID_TESTS
+ and all(status == 'ok' for _, status in ids), 'All19 id cases must report ok')
+ logname = ['test_logname::test_normal', 'test_logname::test_output_format']
+ require(all(re.search(r'^test ' + re.escape(name) + r' \.\.\. ok$', text, re.M)
+ for name in logname), 'Logname owner-CI outcomes are missing')
+ parsed = [{'passed': int(p), 'failed': int(f), 'upstreamIgnored': int(i),
+ 'measured': int(m), 'filteredOut': int(sk)}
+ for _, p, f, i, m, sk in results]
+ return {'status': 'PASS', 'scope': 'Pinned Ubuntu owning-test contract; not all-functional coverage',
+ 'owningTestContract': contract, 'cargoResults': parsed,
+ 'observedTotals': {key: sum(row[key] for row in parsed) for key in parsed[0]},
+ 'idCasesReportedOk': [name for name, _ in ids], 'officialIdRegression': 'PASS',
+ 'lognameCoverage': {'tests': logname, 'ciMarkerForwarded': ci_marker == 'true',
+ 'formalOutcome': 'reported ok', 'genuineLoginFixtureValidated': False,
+ 'limitation': 'The upstream CI guard may return on no login name before login assertions; '
+ 'these outcomes do not establish successful login-name retrieval.'},
+ 'historicalUnfilteredAttempt': {
+ 'sourceSha': 'bc129a88a5ec8c4e64b3ed89bfd97c7e82362d7c',
+ 'runId': 40116776455, 'jobId': 120939862366, 'status': 'FAIL',
+ 'failed': 5, 'idCasesReportedOk': 19, 'upstreamIgnoredActual': 85,
+ 'artifactId': 12570471831,
+ 'artifactSha256': 'd219e2d964ba324bc7a6e27a3b0dd900d566a8b0e428f3ad87ec0855e0936238',
+ 'strictLogSha256': '653a3768ee166f1c221b97886392a7c02178bd2c9e983228de7d4a35ee70925b',
+ 'failedTests': ['test_help_messages_french_translation',
+ 'test_error_messages_french_translation', 'test_french_colored_error_messages',
+ 'test_logname::test_normal', 'test_logname::test_output_format']},
+ 'sha256': digest(log)}
+
+
+def credential_prefix(env, work):
+ prefix = [] if os.geteuid() == 0 else ['sudo', '-n', '--']
+ command = prefix + ['/usr/bin/setpriv', '--reuid=65534', '--rgid=65534',
+ '--egid=1', '--groups=2', '/usr/bin/python3', '-c',
+ 'import os; print(os.getuid(),os.getgid(),os.getegid(),os.getgroups())']
+ require(capture(command, work, env) == '65534 65534 1 [2]',
+ 'Isolated QA needs SETUID/SETGID privileges; no persistent user/group edits')
+ probe = prefix + ['/usr/bin/setpriv', '--reuid=65534', '--rgid=65534', '--egid=1',
+ '--groups=2', '/usr/bin/python3', '-c',
+ 'import os,sys; sys.exit(0 if os.access(sys.argv[1], os.X_OK) else 1)',
+ str(work)]
+ capture(probe, work, env)
+ return prefix
+
+
+def verify_id(binary, prefix, env, work):
+ require(binary.is_file(), 'Packaged id entry point missing')
+ # The directory must be traversable by this bounded unprivileged child.
+ cases = [(65534, 1, [2]), (1, 65534, [1, 2, 65534]), (65534, 65534, [2])]
+ receipts = []
+ for real, effective, supplementary in cases:
+ command = prefix + ['/usr/bin/setpriv', '--reuid=65534', '--rgid=' + str(real),
+ '--egid=' + str(effective),
+ '--groups=' + ','.join(map(str, supplementary)), str(binary)]
+ expected = list(dict.fromkeys([real, effective] + sorted(supplementary)))
+ outputs = {flag: capture(command + flag.split(), work, env)
+ for flag in ['-G', '-G -r', '-g', '-g -r', '']}
+ require(all(list(map(int, outputs[flag].split())) == expected
+ for flag in ['-G', '-G -r']), 'Incorrect real/effective/supplementary GID order')
+ require(outputs['-g'] == str(effective) and outputs['-g -r'] == str(real),
+ 'Single-GID semantics regressed')
+ match = re.search(r' groups=([^\s]+)', outputs[''])
+ actual = [int(x.split('(')[0]) for x in match[1].split(',')] if match else []
+ require(actual == list(dict.fromkeys([effective] + sorted(supplementary))),
+ 'Default groups ordering changed')
+ receipts.append({'realGid': real, 'effectiveGid': effective,
+ 'supplementaryGids': supplementary, 'outputs': outputs, 'status': 'PASS'})
+ return receipts
+
+
+def build(args, lock):
+ release = dict(re.findall(r'^(\w+)=["\']?([^"\'\n]+)',
+ Path('/etc/os-release').read_text(encoding='utf-8'), re.M))
+ require(release.get('ID') == 'ubuntu' and release.get('VERSION_ID') == '26.04',
+ 'Use a disposable Ubuntu 26.04 amd64 builder')
+ require(os.geteuid() != 0,
+ 'Build and full owning suite must run as non-root; grant sudo -n for bounded credential QA')
+ output = args.output_dir.resolve()
+ cache = args.cache_dir.resolve() if args.cache_dir else None
+ work = args.work_dir.resolve() if args.work_dir else Path(tempfile.mkdtemp(prefix='rust-backport-'))
+ require(not output.exists(), 'Output directory must be new')
+ for other in [work, cache]:
+ require(not other or (output != other and output not in other.parents
+ and other not in output.parents), 'Output overlaps build/cache directory')
+ if args.work_dir:
+ work.mkdir(parents=True, exist_ok=False)
+ work.chmod(0o755) # Public source/binaries only; needed for the isolated UID fixture.
+ print('RUST_COREUTILS_BACKPORT_WORK_DIR=' + str(work), flush=True)
+ home = work / 'home'
+ home.mkdir(mode=0o700)
+ env = build_environment(lock, args.jobs, home)
+ for tool in ['dpkg', 'dpkg-source', 'dpkg-buildpackage', 'dpkg-deb', 'dpkg-query',
+ 'dpkg-checkbuilddeps', 'fakeroot', 'make', 'cargo', 'rustc', 'patch', 'readelf', 'setpriv']:
+ require(shutil.which(tool, path=env['PATH']), 'Builder dependency missing: ' + tool)
+ require(capture(['dpkg', '--print-architecture'], work, env) == 'amd64', 'Native amd64 required')
+ rust = capture(['rustc', '--version'], work, env)
+ require(tuple(map(int, re.search(r'rustc (\d+)\.(\d+)\.(\d+)', rust).groups())) >= (1, 88, 0),
+ 'Upstream MSRV is Rust 1.88')
+ prefix = credential_prefix(env, work) # Fail before expensive compilation.
+ inputs = work / 'inputs'
+ inputs.mkdir()
+ if cache:
+ cache.mkdir(parents=True, exist_ok=True)
+ for item in lock['sourceFiles'] + lock['patches']:
+ download(item, inputs, cache)
+ verify_source_control(lock, inputs)
+ source, log = work / 'source', work / 'build.log'
+ run(['dpkg-source', '--extract', str(inputs / ('rust-coreutils_' + VERSION + '.dsc')),
+ str(source)], work, env, log)
+ verify_sources(source, lock['baselineSha256'])
+ test_contract = ubuntu_test_contract(source / 'debian/rules')
+ packaging = packaging_snapshot(source)
+ item = lock['patches'][0]
+ patch = inputs / item['name']
+ verify_patch(item, patch)
+ run(['patch', '-p1', '--fuzz=0', '--batch', '--forward', '--input', str(patch)], source, env, log)
+ verify_sources(source, lock['patchedSha256'])
+ verify_sources(source, packaging)
+ changelog = source / 'debian/changelog'
+ date = email.utils.format_datetime(datetime.datetime.fromtimestamp(
+ lock['sourceDateEpoch'], datetime.timezone.utc))
+ changelog.write_text('rust-coreutils (' + lock['customVersion'] + ') resolute; urgency=high\n\n'
+ ' * First-party PastureStack rebuild; not issued by Canonical.\n'
+ ' * Backport upstream ff382f527994b8a021ec0a086dec4c5cafa34f2a\n'
+ ' for CVE-2026-35370 and CVE-2026-35371; preserve Ubuntu packaging.\n\n'
+ ' -- PastureStack maintainers ' +
+ date + '\n\n' + changelog.read_text(encoding='utf-8'), encoding='utf-8')
+ run(['dpkg-checkbuilddeps'], source, env, log)
+ run(['dpkg-buildpackage', '-b', '-us', '-uc', job_option(args.jobs)], source, env, log)
+ strict_log = work / 'strict-tests.log'
+ run(['make', '-f', str(HERE / 'native-tests.mk'), 'pasturestack-strict-test'],
+ source, env, strict_log)
+ tests = strict_test_receipt(strict_log, test_contract, env.get('CI'))
+ verify_sources(source, packaging)
+ verify_sources(source, lock['patchedSha256'])
+ debs = list(work.glob('rust-coreutils_' + lock['customVersion'] + '_amd64.deb'))
+ require(len(debs) == 1, 'Missing/ambiguous runtime package')
+ deb = debs[0]
+ for field, expected in [('Package', 'rust-coreutils'), ('Version', lock['customVersion']),
+ ('Architecture', 'amd64')]:
+ require(capture(['dpkg-deb', '--field', str(deb), field], work, env) == expected,
+ 'Wrong package ' + field)
+ require(capture(['dpkg-deb', '--field', str(deb), 'Source'], work, env) in
+ ['', 'rust-coreutils', 'rust-coreutils (' + lock['customVersion'] + ')'], 'Wrong Source identity')
+ extracted = work / 'extracted'
+ run(['dpkg-deb', '--extract', str(deb), str(extracted)], work, env, log)
+ binary = extracted / 'usr/lib/cargo/bin/coreutils/id'
+ credentials = verify_id(binary, prefix, env, work)
+ require(capture([str(binary), '--version'], work, env).startswith('id (uutils coreutils) 0.10.0'),
+ 'Unexpected upstream binary version')
+ copyright_file = extracted / 'usr/share/doc/rust-coreutils/copyright'
+ require(copyright_file.is_file() and digest(copyright_file) == digest(source / 'debian/copyright'),
+ 'Original copyright changed')
+ headers = capture(['readelf', '--program-headers', str(binary)], work, env)
+ dynamic = capture(['readelf', '--dynamic', str(binary)], work, env)
+ require('GNU_RELRO' in headers and ('BIND_NOW' in dynamic or re.search(r'FLAGS_1.*NOW', dynamic)),
+ 'Package binary lacks RELRO/immediate-binding hardening')
+ output.mkdir(parents=True)
+ for file in [deb, log, strict_log, source / 'debian/copyright']:
+ shutil.copyfile(file, output / file.name)
+ shutil.copyfile(LOCK_FILE, output / 'rust-coreutils.lock.json')
+ for pattern in ['rust-coreutils_*.buildinfo', 'rust-coreutils_*.changes']:
+ matches = list(work.glob(pattern))
+ require(len(matches) == 1, 'Missing/ambiguous package receipt: ' + pattern)
+ shutil.copyfile(matches[0], output / matches[0].name)
+ (output / 'builder-packages.tsv').write_text(capture(
+ ['dpkg-query', '--show', '--showformat=${binary:Package}\t${Version}\n'], work, env) + '\n',
+ encoding='utf-8')
+ receipt = {'schemaVersion': 1, 'status': 'native_build_and_required_tests_passed',
+ 'builtAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
+ 'identity': 'first-party rebuild; not issued by Canonical',
+ 'lockSha256': digest(LOCK_FILE), 'package': deb.name, 'packageSha256': digest(deb),
+ 'rustc': rust, 'strictOwningSuite': tests, 'distinctCredentialBinaryTests': credentials,
+ 'alreadyPresentFixes': lock['alreadyPresentFixes'],
+ 'newlyBackportedCves': item['fixedCves'], 'packagingPreserved': packaging,
+ 'runtimeInstalled': False, 'alternativesChanged': False}
+ (output / 'provenance.json').write_text(json.dumps(receipt, indent=2) + '\n', encoding='utf-8')
+ (output / 'SHA256SUMS').write_text(''.join(digest(p) + ' ' + p.name + '\n'
+ for p in sorted(output.iterdir()) if p.is_file()), encoding='utf-8')
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ mode = parser.add_mutually_exclusive_group()
+ mode.add_argument('--validate-lock', action='store_true')
+ mode.add_argument('--print-ubuntu-test-arguments', 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()
+ try:
+ lock = load_lock(LOCK_FILE)
+ require(1 <= args.jobs <= 64, 'Invalid job count')
+ if args.print_ubuntu_test_arguments:
+ print(ubuntu_test_arguments(ubuntu_test_contract(Path.cwd() / 'debian/rules')))
+ return
+ if args.validate_lock:
+ print('Rust-coreutils source/patch lock: PASS')
+ return
+ require(args.output_dir is not None, '--output-dir is required')
+ build(args, lock)
+ except (ValueError, OSError, subprocess.SubprocessError) as error:
+ parser.exit(1, str(error) + '\n')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/server/security/ubuntu-backports/rust-coreutils/native-tests.mk b/server/security/ubuntu-backports/rust-coreutils/native-tests.mk
new file mode 100644
index 0000000000..60c499e23c
--- /dev/null
+++ b/server/security/ubuntu-backports/rust-coreutils/native-tests.mk
@@ -0,0 +1,13 @@
+# Retain Ubuntu exports/features and its pinned 17 exclusions. Remove only the
+# accidental positional filter and failure swallowing from its test recipe.
+override _PS_TEST_HELPER := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/build-rust-coreutils-backport.py)
+include debian/rules
+override _PS_TEST_ARGS := $(shell python3 "$(_PS_TEST_HELPER)" --print-ubuntu-test-arguments)
+ifneq ($(.SHELLSTATUS),0)
+$(error Pinned Ubuntu owning-test arguments could not be verified)
+endif
+override TEST_NO_FAIL_FAST := $(_PS_TEST_ARGS)
+
+.PHONY: pasturestack-strict-test
+pasturestack-strict-test:
+ $(MAKE) -f GNUmakefile SELINUX_ENABLED=1 PROFILE=release MULTICALL=y TEST_NO_FAIL_FAST="$(TEST_NO_FAIL_FAST)" test
diff --git a/server/security/ubuntu-backports/rust-coreutils/rust-coreutils.lock.json b/server/security/ubuntu-backports/rust-coreutils/rust-coreutils.lock.json
new file mode 100644
index 0000000000..62ec67058d
--- /dev/null
+++ b/server/security/ubuntu-backports/rust-coreutils/rust-coreutils.lock.json
@@ -0,0 +1,266 @@
+{
+ "schemaVersion": 1,
+ "sourcePackage": "rust-coreutils",
+ "ubuntuSourceVersion": "0.10.0-1ubuntu2~26.04.1",
+ "customVersion": "0.10.0-1ubuntu2~26.04.1+pasturestack.1",
+ "distribution": "resolute",
+ "osVersion": "26.04",
+ "architecture": "amd64",
+ "runtimePackages": [
+ "rust-coreutils"
+ ],
+ "sourceDateEpoch": 1792886400,
+ "sourcePublication": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/18747163",
+ "sourceFiles": [
+ {
+ "name": "rust-coreutils_0.10.0-1ubuntu2~26.04.1.dsc",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0-1ubuntu2~26.04.1.dsc",
+ "sha256": "c7eb62e7d2a07ec84c18bb2a6694331bb32bd0bde316261cfbe2f90acb2b6d28",
+ "size": 8746
+ },
+ {
+ "name": "rust-coreutils_0.10.0.orig.tar.gz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0.orig.tar.gz",
+ "sha256": "f8e68cd0e3629378f047544ead272161a83211c43f4985a9f52944e5db8f1a44",
+ "size": 2671039
+ },
+ {
+ "name": "rust-coreutils_0.10.0.orig-l10n.tar.gz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0.orig-l10n.tar.gz",
+ "sha256": "3df90fabddfb28c7ae1d31237fc8abf70bb1f6c1937d818008943a11e31cfd5b",
+ "size": 780526
+ },
+ {
+ "name": "rust-coreutils_0.10.0-1ubuntu2~26.04.1.debian.tar.xz",
+ "url": "https://archive.ubuntu.com/ubuntu/pool/main/r/rust-coreutils/rust-coreutils_0.10.0-1ubuntu2~26.04.1.debian.tar.xz",
+ "sha256": "61eda389122a85e90e542bf6b12c840ab68ee3741958db539f24a3599f5d4bb2",
+ "size": 22541108
+ }
+ ],
+ "upstream": {
+ "repository": "https://github.com/uutils/coreutils",
+ "baseTag": "0.10.0",
+ "baseCommit": "28b6856d7b215bf844b4223589cb54ade84f5223",
+ "archiveUrl": "https://github.com/uutils/coreutils/archive/refs/tags/0.10.0.tar.gz",
+ "archiveSha256": "f8e68cd0e3629378f047544ead272161a83211c43f4985a9f52944e5db8f1a44",
+ "archiveEqualsUbuntuOrig": true,
+ "releaseUrl": "https://github.com/uutils/coreutils/releases/tag/0.10.0"
+ },
+ "alreadyPresentFixes": [
+ {
+ "id": "CVE-2026-35341",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.6.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/10376",
+ "commit": "00f77cc7081f8e6837271d21b4fe8ca6d2f047c3",
+ "sourcePath": "src/uu/mkfifo/src/mkfifo.rs"
+ },
+ {
+ "id": "CVE-2026-35344",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12667",
+ "commit": "0f538b942a5cdaba6e0960f6952429a1baaedf93",
+ "sourcePath": "src/uu/dd/src/dd.rs"
+ },
+ {
+ "id": "CVE-2026-35345",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12661",
+ "commit": "2c2f1b0c1f11f120fc8cbf8d518194f6532c0607",
+ "sourcePath": "src/uu/tail/src/tail.rs"
+ },
+ {
+ "id": "CVE-2026-35348",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/11593",
+ "commit": "bb8f7095fd63206ff54a4fb4256375e9e1ad88c9",
+ "sourcePath": "src/uu/sort/src/sort.rs",
+ "additionalCommits": [
+ "6e28633263ea84aad9ef7a30750232b73fb9133f"
+ ]
+ },
+ {
+ "id": "CVE-2026-35350",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12135",
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "sourcePath": "src/uu/cp/src/cp.rs"
+ },
+ {
+ "id": "CVE-2026-35351",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/11706",
+ "commit": "874efa7cc3361cb5af2a97db869147f910bcab44",
+ "sourcePath": "src/uu/mv/src/mv.rs"
+ },
+ {
+ "id": "CVE-2026-35352",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12174",
+ "commit": "c004672c5eb2415b03f11f766583c4b967ba024f",
+ "sourcePath": "src/uu/mkfifo/src/mkfifo.rs"
+ },
+ {
+ "id": "CVE-2026-35354",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12170",
+ "commit": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "sourcePath": "src/uu/mv/src/mv.rs"
+ },
+ {
+ "id": "CVE-2026-35357",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12135",
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "sourcePath": "src/uu/cp/src/cp.rs"
+ },
+ {
+ "id": "CVE-2026-35359",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12135",
+ "commit": "bd170a5d4b19ed985417d322dffb8cade2cbbad5",
+ "sourcePath": "src/uu/cp/src/cp.rs"
+ },
+ {
+ "id": "CVE-2026-35360",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12171",
+ "commit": "d296f8d39f3a71ef7c5bd3b949e65aa978f1436d",
+ "sourcePath": "src/uu/touch/src/touch.rs"
+ },
+ {
+ "id": "CVE-2026-35363",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/11005",
+ "commit": "6b16cc9688fe998f5dc2786f53f22ceea709e318",
+ "sourcePath": "src/uu/rm/src/rm.rs"
+ },
+ {
+ "id": "CVE-2026-35364",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12170",
+ "commit": "aa30c103004463ffe7b8a98ca0ce519d56bbbd41",
+ "sourcePath": "src/uu/mv/src/mv.rs"
+ },
+ {
+ "id": "CVE-2026-35367",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12339",
+ "commit": "acd51ef18d90e5c26f694df721f896fa1c4a9edc",
+ "sourcePath": "src/uu/nohup/src/nohup.rs"
+ },
+ {
+ "id": "CVE-2026-35368",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.9.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/11211",
+ "commit": "f33bfb9321e5ebef0ef703aa6bfc8ee95bd21e1f",
+ "sourcePath": "src/uu/chroot/src/chroot.rs"
+ },
+ {
+ "id": "CVE-2026-35373",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/11403",
+ "commit": "48d030dfb2ca7659ee2735f09cda706c8245a3fc",
+ "sourcePath": "src/uu/ln/src/ln.rs"
+ },
+ {
+ "id": "CVE-2026-35374",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12565",
+ "commit": "58040feb5c66fce588a7ffac299b3a23bd55cd81",
+ "sourcePath": "src/uu/split/src/split.rs"
+ },
+ {
+ "id": "CVE-2026-35377",
+ "status": "already_in_original_ubuntu_source",
+ "firstFixedVersion": "0.10.0",
+ "upstreamPullRequest": "https://github.com/uutils/coreutils/pull/12657",
+ "commit": "b9d8ff64c224eb425702584b66ec0ac64a99592a",
+ "sourcePath": "src/uu/env/src/env.rs"
+ }
+ ],
+ "patches": [
+ {
+ "name": "ff382f527994b8a021ec0a086dec4c5cafa34f2a.patch",
+ "commit": "ff382f527994b8a021ec0a086dec4c5cafa34f2a",
+ "url": "https://github.com/uutils/coreutils/commit/ff382f527994b8a021ec0a086dec4c5cafa34f2a.patch",
+ "sha256": "32d843876e0814cb070104afc049c036391604ea517c6e837ffc9345fe1e2de2",
+ "size": 4082,
+ "firstFixedVersion": "0.11.0",
+ "fixedCves": [
+ "CVE-2026-35370",
+ "CVE-2026-35371"
+ ],
+ "pullRequest": "https://github.com/uutils/coreutils/pull/14103",
+ "paths": [
+ "src/uu/id/src/id.rs",
+ "tests/by-util/test_id.rs"
+ ]
+ }
+ ],
+ "baselineSha256": {
+ "src/uu/id/src/id.rs": "de9b78d3d511f5c5ac14f42338648f8755ac5b99cae2376cf2af18e9cce5c3f0",
+ "tests/by-util/test_id.rs": "d6948f672f30bf9205fa87526ce9efcdabc9950910269e5cdef458b914d65b4e",
+ "src/uu/mkfifo/src/mkfifo.rs": "6c7c49a911cff80e6ea0499044971bd232c37b298a5a0c83551b41d5a9f46dbe",
+ "src/uu/dd/src/dd.rs": "11052025db5fd3e1c5cac0b026c67b1d45c7cf601cff01329eba0f2408ad54b9",
+ "src/uu/tail/src/tail.rs": "8168856be7226284452e65604fa6da162a137050cd2c1822e8c3aac87b7904b0",
+ "src/uu/sort/src/sort.rs": "95820b55661c4281b3a51e54496fb9c0f5050ed202f1e8c68b87efb3e17de5dd",
+ "src/uu/cp/src/cp.rs": "7094892394071883367c3b0de3b1b7d0beee6a354d32931397808276547ae525",
+ "src/uu/mv/src/mv.rs": "852b6f61b68bc9b96ead38f1132ae23047a14e4229705a8f11f31f5ba1608734",
+ "src/uu/touch/src/touch.rs": "5a348cc8d276aa0613393ea0451fca703d5e614ae83e6cae6b25582cf0cbda27",
+ "src/uu/rm/src/rm.rs": "776b445347e3087e90d811b38b5b7604c8e362608c1772b762aa7aabe55671c4",
+ "src/uu/nohup/src/nohup.rs": "93a8ab0c82728d3856b0e181312bdb3f0418d86a33f70af00b61e915e2b3173c",
+ "src/uu/chroot/src/chroot.rs": "9282c030345eee3c85c029b0978f587452087c4c873d3ed2f527c4aa677b4e7c",
+ "src/uu/ln/src/ln.rs": "5c681cfa583a268bdce9a9b740f9dcd8b38c5ab53754883e5c20800259a4f1b3",
+ "src/uu/split/src/split.rs": "8ad1864e2abaac40dfc3523f29ea4013624e872dfd65e0c8c40e2bc1b38b4e75",
+ "src/uu/env/src/env.rs": "2cc9122003a63d95a439d39c0ab074409bca4098bc930a38c4e6cc5a625543e1",
+ "GNUmakefile": "eb532c745680e9c82a6836692b306e3a29b1fec5f1055e341aa3d41490acb459",
+ "debian/rules": "06c5fd183a4a1146748810e3cce859ba849643eeee244cc80cbb3315aeedb5e7",
+ "debian/copyright": "f1ea5d8870ff83301cc4c129ecd5e0e3c998bd566a9b0036df899e84d9336bd6"
+ },
+ "patchedSha256": {
+ "src/uu/id/src/id.rs": "5a8ec6db1ac0ac370c6557ef2fe4832d9b5f2942e6c9e60f086a1c08f90a7468",
+ "tests/by-util/test_id.rs": "75fac73627073f8a33f086058510db446efc5226d8b9d3c944ae1a2b8caf8689"
+ },
+ "buildContract": {
+ "nativeOnly": true,
+ "buildAndFullSuiteUser": "non-root",
+ "credentialFixturePrivilege": "sudo -n setpriv; bounded subprocess only",
+ "minRustVersion": "1.88.0",
+ "originalUbuntuPackaging": true,
+ "profile": "release",
+ "multicall": true,
+ "selinuxEnabled": true,
+ "lto": "thin",
+ "optLevel": "z",
+ "codegenUnits": 1,
+ "releaseDebug": true,
+ "network": "Cargo offline; vendored crates from the pinned Ubuntu Debian archive",
+ "strictTestTarget": "pasturestack-strict-test",
+ "newSkips": 0,
+ "installRuntime": false,
+ "alterAlternatives": false
+ },
+ "limitations": [
+ "This is a first-party rebuild candidate, not a Canonical-issued update.",
+ "The original Ubuntu test recipe has skips and ||true; package-build success alone is not sufficient. The helper additionally runs the complete owning suite without added skips and requires success.",
+ "Source SHA pins establish reviewed source identity. They do not claim verified PGP or upstream commit signatures.",
+ "Source preparation and zero-fuzz patch application are complete; native build, strict full tests and distinct-credential binary QA remain required before any remediation claim."
+ ]
+}
diff --git a/server/security/ubuntu-backports/rust-coreutils/test_rust_coreutils_backport.py b/server/security/ubuntu-backports/rust-coreutils/test_rust_coreutils_backport.py
new file mode 100644
index 0000000000..6bdf8dab77
--- /dev/null
+++ b/server/security/ubuntu-backports/rust-coreutils/test_rust_coreutils_backport.py
@@ -0,0 +1,202 @@
+"""Network-free guards for source identity, unsuppressed tests and GID QA."""
+import copy
+import importlib.util
+import json
+import os
+import shlex
+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('rust_backport', HERE / 'build-rust-coreutils-backport.py')
+M = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(M)
+
+
+class RustBackport(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix='rust-backport-contract-')
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name)
+ self.lock = M.load_lock(HERE / 'rust-coreutils.lock.json')
+
+ def test_lock_rejects_widened_scope_unpinned_sources_and_wrong_identity(self):
+ for field, bad in [('architecture', 'arm64'), ('ubuntuSourceVersion', '0.12.0'),
+ ('runtimePackages', ['coreutils']), ('sourceDateEpoch', True)]:
+ with self.subTest(field=field):
+ altered = copy.deepcopy(self.lock)
+ altered[field] = bad
+ file = self.root / 'bad.json'
+ file.write_text(json.dumps(altered), encoding='utf-8')
+ with self.assertRaises(ValueError): M.load_lock(file)
+ for bad in ['../outside.patch', 'http://example.invalid/patch', 'bad-hash']:
+ altered = copy.deepcopy(self.lock)
+ key = 'name' if bad.startswith('..') else ('url' if bad.startswith('http') else 'sha256')
+ altered['patches'][0][key] = bad
+ file.write_text(json.dumps(altered), encoding='utf-8')
+ with self.assertRaises(ValueError): M.load_lock(file)
+
+ def test_poisoned_cache_is_not_silently_refetched(self):
+ cache, inputs = self.root / 'cache', self.root / 'inputs'
+ cache.mkdir(); inputs.mkdir()
+ item = self.lock['patches'][0]
+ (cache / item['name']).write_bytes(b'poison')
+ with mock.patch.object(M.urllib.request, 'build_opener') as opener:
+ with self.assertRaises(ValueError): M.download(item, inputs, cache)
+ opener.assert_not_called()
+
+ def test_source_control_manifest_must_match_all_three_pinned_archives(self):
+ entries = [x for x in self.lock['sourceFiles'] if not x['name'].endswith('.dsc')]
+ text = ('Format: 3.0 (quilt)\nSource: rust-coreutils\nVersion: ' + M.VERSION +
+ '\nChecksums-Sha256:\n' + ''.join(' ' + x['sha256'] + ' ' + str(x['size']) +
+ ' ' + x['name'] + '\n' for x in entries))
+ file = self.root / ('rust-coreutils_' + M.VERSION + '.dsc')
+ file.write_text(text, encoding='utf-8')
+ M.verify_source_control(self.lock, self.root)
+ file.write_text(text.replace(entries[0]['sha256'], '0' * 64), encoding='utf-8')
+ with self.assertRaises(ValueError): M.verify_source_control(self.lock, self.root)
+
+ def test_patch_cannot_modify_packaging_or_other_utilities(self):
+ file = self.root / 'patch'
+ text = 'From ' + M.COMMIT + ' Mon Sep 17 00:00:00 2001\n' + ''.join(
+ 'diff --git a/' + p + ' b/' + p + '\n' for p in M.PATHS)
+ file.write_text(text, encoding='utf-8')
+ M.verify_patch(self.lock['patches'][0], file)
+ file.write_text(text + 'diff --git a/debian/rules b/debian/rules\n', encoding='utf-8')
+ with self.assertRaises(ValueError): M.verify_patch(self.lock['patches'][0], file)
+
+ def test_build_environment_discards_test_and_compiler_overrides(self):
+ with mock.patch.dict(os.environ, {'DEB_BUILD_OPTIONS': 'nocheck', 'CARGOFLAGS': '--test id',
+ 'RUSTFLAGS': '-C opt-level=0', 'TESTS': 'id', 'CI': 'true',
+ 'TEST_NO_FAIL_FAST': 'stdout', '_PS_TEST_ARGS': '--ignored'}):
+ env = M.build_environment(self.lock, 2, self.root)
+ self.assertEqual('parallel=2', env['DEB_BUILD_OPTIONS'])
+ self.assertEqual('', env['DEB_BUILD_MAINT_OPTIONS'])
+ self.assertEqual('true', env['CARGO_NET_OFFLINE'])
+ self.assertEqual('true', env['CI'])
+ self.assertTrue(all(x not in env for x in ['CARGOFLAGS', 'RUSTFLAGS', 'TESTS', 'SKIP_UTILS',
+ 'TEST_NO_FAIL_FAST', '_PS_TEST_ARGS']))
+ for marker in ['', 'false', '1']:
+ with mock.patch.dict(os.environ, {'CI': marker}):
+ self.assertNotIn('CI', M.build_environment(self.lock, 2, self.root))
+
+ def test_root_cannot_accidentally_run_the_build_or_full_suite(self):
+ with mock.patch.object(Path, 'read_text', return_value='ID=ubuntu\nVERSION_ID="26.04"\n'), \
+ mock.patch.object(M.os, 'geteuid', create=True, return_value=0):
+ with self.assertRaisesRegex(ValueError, 'must run as non-root'):
+ M.build(None, self.lock)
+
+ def contract_fixture(self):
+ patterns = list(M.UBUNTU_EXCLUSIONS)
+ patterns[11] += ' stdout'
+ args = '-- --skip ' + ' \\\n--skip '.join(patterns)
+ return 'override_dh_auto_test:\n TEST_NO_FAIL_FAST="' + args + '"\n test||true\n' + \
+ 'override_dh_auto_install:\n'
+
+ def contract(self):
+ rules = self.root / 'rules'
+ rules.write_text(self.contract_fixture(), encoding='utf-8')
+ with mock.patch.object(M, 'digest', return_value=M.RULES_SHA256):
+ return M.ubuntu_test_contract(rules)
+
+ def test_original_contract_requires_pinned_rules_and_exact17_patterns(self):
+ rules = self.root / 'rules'
+ rules.write_text(self.contract_fixture(), encoding='utf-8')
+ with self.assertRaises(ValueError): M.ubuntu_test_contract(rules)
+ contract = self.contract()
+ self.assertEqual(17, len(contract['excludedPatterns']))
+ self.assertEqual(['stdout'], contract['removedLegacyPositionalFilters'])
+ self.assertTrue(contract['removedFailureSwallowing'])
+ self.assertFalse(any(pattern in name for pattern in contract['excludedPatterns']
+ for name in M.ID_TESTS))
+
+ def test_contract_rejects_new_missing_reordered_id_or_extra_filters(self):
+ original = self.contract_fixture()
+ first, second = M.UBUNTU_EXCLUSIONS[:2]
+ for bad in [original.replace(first, 'test_id::'), original.replace('--skip ' + first, ''),
+ original.replace(first, 'marker').replace(second, first).replace('marker', second),
+ original.replace(' stdout', ''), original.replace(' stdout', ' stdout --ignored'),
+ original.replace('test||true', 'test')]:
+ rules = self.root / 'rules'
+ rules.write_text(bad, encoding='utf-8')
+ with mock.patch.object(M, 'digest', return_value=M.RULES_SHA256):
+ with self.assertRaises(ValueError): M.ubuntu_test_contract(rules)
+
+ def test_command_contains_only_owner_exclusions_without_positional_filter_or_swallow(self):
+ contract = self.contract()
+ tokens = shlex.split(M.ubuntu_test_arguments(contract))
+ self.assertEqual(['--no-fail-fast', '--'], tokens[:2])
+ self.assertEqual(17, tokens.count('--skip'))
+ self.assertEqual(M.UBUNTU_EXCLUSIONS, tokens[3::2])
+ self.assertNotIn('stdout', tokens)
+ self.assertNotIn('||true', tokens)
+ altered = copy.deepcopy(contract)
+ altered['excludedPatterns'].append('test_id')
+ with self.assertRaises(ValueError): M.ubuntu_test_arguments(altered)
+
+ def owner_log(self, main_ignored=83):
+ targets = ['Running unittests src/lib.rs', 'Running unittests src/bin/coreutils.rs',
+ 'Running tests/test_localization_and_colors.rs', 'Running tests/test_util_name.rs',
+ 'Running tests/test_uudoc.rs', 'Running tests/tests.rs', 'Doc-tests coreutils']
+ ignored = [0, 0, 0, 2, 0, main_ignored, 0]
+ lines = [line for target, count in zip(targets, ignored) for line in
+ [target, f'test result: ok. 30 passed; 0 failed; {count} ignored; 0 measured; 0 filtered out;']]
+ lines += ['test ' + name + ' ... ok' for name in M.ID_TESTS]
+ lines += ['test test_logname::test_normal ... ok', 'test test_logname::test_output_format ... ok']
+ return '\n'.join(lines) + '\n'
+
+ def test_strict_receipt_requires_all_targets_and_all19_id_cases(self):
+ file = self.root / 'strict.log'
+ contract = self.contract()
+ good = self.owner_log()
+ file.write_text(good, encoding='utf-8')
+ self.assertEqual('PASS', M.strict_test_receipt(file, contract, 'true')['status'])
+ for bad in [good.replace('groups_ordering', 'unrelated'),
+ good.replace('test_id::test_id_real ... ok', 'test_id::test_id_real ... ignored'),
+ good.replace('Running tests/test_util_name.rs', 'unrelated target'),
+ good.replace('0 failed', '1 failed'), good + 'test result: FAILED\n', '']:
+ file.write_text(bad, encoding='utf-8')
+ with self.assertRaises(ValueError): M.strict_test_receipt(file, contract, 'true')
+
+ def test_metadata_preserves_old_fail_85ignored_and_owner_ci_coverage_limits(self):
+ file = self.root / 'strict.log'
+ file.write_text(self.owner_log(main_ignored=80), encoding='utf-8')
+ receipt = M.strict_test_receipt(file, self.contract(), 'true')
+ self.assertEqual(17, len(receipt['owningTestContract']['excludedPatterns']))
+ self.assertEqual(82, receipt['observedTotals']['upstreamIgnored'])
+ self.assertEqual(85, receipt['historicalUnfilteredAttempt']['upstreamIgnoredActual'])
+ self.assertEqual('FAIL', receipt['historicalUnfilteredAttempt']['status'])
+ self.assertEqual(5, receipt['historicalUnfilteredAttempt']['failed'])
+ self.assertEqual(19, len(receipt['idCasesReportedOk']))
+ self.assertTrue(receipt['lognameCoverage']['ciMarkerForwarded'])
+ self.assertFalse(receipt['lognameCoverage']['genuineLoginFixtureValidated'])
+
+ def test_native_command_failure_cannot_be_swallowed(self):
+ with self.assertRaises(ValueError):
+ M.run([sys.executable, '-c', 'raise SystemExit(3)'], self.root, dict(os.environ),
+ self.root / 'failed.log')
+
+ def test_distinct_gid_fixture_detects_original_effective_first_bug(self):
+ binary = self.root / 'id'
+ binary.write_bytes(b'fixture')
+ def output(command, *_):
+ real = int(next(x.split('=')[1] for x in command if x.startswith('--rgid=')))
+ effective = int(next(x.split('=')[1] for x in command if x.startswith('--egid=')))
+ extra = sorted(map(int, next(x.split('=')[1] for x in command if x.startswith('--groups=')).split(',')))
+ args = command[command.index(str(binary)) + 1:]
+ if '-G' in args: return ' '.join(map(str, dict.fromkeys([real, effective] + extra)))
+ if '-g' in args: return str(real if '-r' in args else effective)
+ return 'uid=65534(nobody) gid=' + str(real) + ' groups=' + ','.join(
+ str(x) + '(group)' for x in dict.fromkeys([effective] + extra))
+ with mock.patch.object(M, 'capture', side_effect=output):
+ self.assertEqual(3, len(M.verify_id(binary, [], {}, self.root)))
+ with mock.patch.object(M, 'capture', return_value='1 2'):
+ with self.assertRaises(ValueError): M.verify_id(binary, [], {}, self.root)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/server/security/ubuntu-backports/source_helpers.py b/server/security/ubuntu-backports/source_helpers.py
new file mode 100644
index 0000000000..5edda12341
--- /dev/null
+++ b/server/security/ubuntu-backports/source_helpers.py
@@ -0,0 +1,245 @@
+"""Small shared source/deb operations for the p11-kit and libXrender backports.
+
+This imports the already reviewed PCRE2 pure helpers. It does not change any
+existing module or perform package installation.
+"""
+import argparse
+import datetime
+import email.utils
+import importlib.util
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+HERE = Path(__file__).resolve().parent
+SPEC = importlib.util.spec_from_file_location('ubuntu_backport_common', HERE / 'build-pcre2-backport.py')
+COMMON = importlib.util.module_from_spec(SPEC)
+_bytecode = sys.dont_write_bytecode
+sys.dont_write_bytecode = True
+try:
+ SPEC.loader.exec_module(COMMON)
+finally:
+ sys.dont_write_bytecode = _bytecode
+require, digest, run, capture = COMMON.require, COMMON.digest, COMMON.run, COMMON.capture
+
+
+def inputs(lock):
+ return lock['sourceFiles'] + lock['patches'] + ([lock['baselineBinary']] if 'baselineBinary' in lock else [])
+
+
+def load_lock(file, package, version, commits):
+ lock = json.loads(file.read_text(encoding='utf-8'))
+ require(all(lock.get(k) == v for k, v in {
+ 'schemaVersion': 1, 'sourcePackage': package, 'ubuntuSourceVersion': version,
+ 'customVersion': version + '+pasturestack.1', 'distribution': 'resolute',
+ 'osVersion': '26.04', 'architecture': 'amd64'}.items()), 'Wrong backport identity')
+ require(type(lock['sourceDateEpoch']) is int and lock['sourceDateEpoch'] > 0, 'Invalid source date')
+ require([p['commit'] for p in lock['patches']] == commits, 'Unexpected security commit set/order')
+ expected_names = ({'p11-kit_0.26.2-2.dsc', 'p11-kit_0.26.2.orig.tar.xz',
+ 'p11-kit_0.26.2.orig.tar.xz.asc', 'p11-kit_0.26.2-2.debian.tar.xz'}
+ if package == 'p11-kit' else
+ {'libxrender_0.9.12-1build1.dsc', 'libxrender_0.9.12.orig.tar.gz',
+ 'libxrender_0.9.12.orig.tar.gz.asc', 'libxrender_0.9.12-1build1.diff.gz'})
+ require(len(lock['sourceFiles']) == 4 and {p['name'] for p in lock['sourceFiles']} == expected_names,
+ 'Wrong original Ubuntu source set')
+ expected_packages = (['libp11-kit0', 'p11-kit', 'p11-kit-modules'] if package == 'p11-kit'
+ else ['libxrender1'])
+ require(lock['runtimePackages'] == expected_packages, 'Unexpected exported package set')
+ require(('baselineBinary' in lock) == (package == 'libxrender'), 'Unexpected baseline binary scope')
+ if package == 'libxrender':
+ require(lock['baselineBinary']['name'] == 'libxrender1_0.9.12-1build1_amd64.deb'
+ and lock['baselineBinary']['version'] == version, 'Wrong original Ubuntu ABI baseline')
+ pool = 'p/p11-kit' if package == 'p11-kit' else 'libx/libxrender'
+ for item in inputs(lock):
+ require(re.fullmatch(r'[A-Za-z0-9._~+-]+', item['name'])
+ and re.fullmatch(r'[0-9a-f]{64}', item['sha256'])
+ and type(item['size']) is int and 0 < item['size'] < 8 * 1024 * 1024,
+ 'Unpinned/unsafe source')
+ if item in lock['sourceFiles'] or item == lock.get('baselineBinary'):
+ url = 'https://archive.ubuntu.com/ubuntu/pool/main/' + pool + '/' + item['name']
+ else:
+ require(item['name'] == item['commit'] + '.patch', 'Patch filename/commit mismatch')
+ url = ('https://github.com/p11-glue/p11-kit/commit/' + item['commit'] + '.patch'
+ if package == 'p11-kit' else
+ 'https://gitlab.freedesktop.org/xorg/lib/libxrender/-/commit/' +
+ item['commit'] + '.patch')
+ require(item['url'] == url, 'Unexpected official source URL')
+ require(set(lock['baselineSha256']) == set(lock['patchedSha256']), 'Changed patch boundary')
+ for group in ['baselineSha256', 'patchedSha256', 'ubuntuPackagingSha256']:
+ for name, sha in lock[group].items():
+ require(not Path(name).is_absolute() and '..' not in Path(name).parts
+ and re.fullmatch(r'[0-9a-f]{64}', sha), 'Unsafe/unpinned source path')
+ return lock
+
+
+def verify_hashes(root, values):
+ require(all((root / name).is_file() and digest(root / name) == sha
+ for name, sha in values.items()), 'Source/packaging bytes changed')
+
+
+def verify_inputs(lock, directory):
+ for item in inputs(lock):
+ file = directory / item['name']
+ require(file.is_file() and file.stat().st_size == item['size']
+ and digest(file) == item['sha256'], 'Input mismatch: ' + item['name'])
+ file = next(p for p in lock['sourceFiles'] if p['name'].endswith('.dsc'))
+ text = (directory / file['name']).read_text(encoding='utf-8')
+ require(all(re.search('^' + re.escape(value) + '$', text, re.M) for value in
+ ['Source: ' + lock['sourcePackage'], 'Version: ' + lock['ubuntuSourceVersion']]),
+ 'Wrong Ubuntu source control identity')
+ block = re.search(r'^Checksums-Sha256:\n((?: .+\n)+)', text, re.M)
+ require(block, 'Missing .dsc SHA-256 manifest')
+ require({n: (h, int(s)) for h, s, n in
+ re.findall(r'^ ([0-9a-f]{64}) (\d+) (\S+)$', block[1], re.M)} ==
+ {p['name']: (p['sha256'], p['size']) for p in lock['sourceFiles']
+ if not p['name'].endswith('.dsc')}, 'Ubuntu .dsc/lock manifest mismatch')
+ for patch in lock['patches']:
+ text = (directory / patch['name']).read_text(encoding='utf-8')
+ require(text.startswith('From ' + patch['commit'] + ' '), 'Wrong upstream patch commit')
+ paths = re.findall(r'^diff --git a/(\S+) b/(\S+)$', text, re.M)
+ require(paths and all(a == b and a in lock['baselineSha256'] for a, b in paths),
+ 'Patch changed unapproved source/test paths')
+
+
+def prepare(lock, directory, work, env, log):
+ source = work / 'source'
+ dsc = next(p['name'] for p in lock['sourceFiles'] if p['name'].endswith('.dsc'))
+ run(['dpkg-source', '--extract', str(directory / dsc), str(source)], work, env, log)
+ verify_hashes(source, lock['baselineSha256'])
+ verify_hashes(source, lock['ubuntuPackagingSha256'])
+ packaging = COMMON.packaging_snapshot(source)
+ if lock['sourcePackage'] == 'libxrender':
+ shutil.copyfile(source / 'src/Xrender.c', work / 'original-Xrender.c')
+ for patch in lock['patches']:
+ run(['patch', '-p1', '--fuzz=0', '--batch', '--forward', '--input',
+ str(directory / patch['name'])], source, env, log)
+ verify_hashes(source, lock['patchedSha256'])
+ verify_hashes(source, packaging)
+ changelog = source / 'debian/changelog'
+ date = email.utils.format_datetime(datetime.datetime.fromtimestamp(
+ lock['sourceDateEpoch'], datetime.timezone.utc))
+ changelog.write_text(lock['sourcePackage'] + ' (' + lock['customVersion'] +
+ ') resolute; urgency=high\n\n'
+ ' * First-party PastureStack rebuild; not issued by Canonical.\n'
+ ' * Apply exact upstream security commits; preserve Ubuntu packaging.\n\n'
+ ' -- PastureStack maintainers ' +
+ date + '\n\n' + changelog.read_text(encoding='utf-8'), encoding='utf-8')
+ return source, packaging
+
+
+def extract_packages(lock, source, work, env, log):
+ extracted, selected = work / 'extracted', []
+ version = lock['customVersion'].split(':')[-1]
+ for name in lock['runtimePackages']:
+ matches = list(work.glob(name + '_' + version + '_amd64.deb'))
+ require(len(matches) == 1, 'Missing/ambiguous runtime package: ' + name)
+ file = matches[0]
+ for field, value in [('Package', name), ('Version', lock['customVersion']), ('Architecture', 'amd64')]:
+ require(capture(['dpkg-deb', '--field', str(file), field], work, env) == value,
+ 'Runtime package metadata changed: ' + field)
+ identity = capture(['dpkg-deb', '--field', str(file), 'Source'], work, env)
+ require(identity in [lock['sourcePackage'], lock['sourcePackage'] + ' (' +
+ lock['customVersion'] + ')'] or
+ (name == lock['sourcePackage'] and identity == ''), 'Wrong Source identity')
+ run(['dpkg-deb', '--extract', str(file), str(extracted)], work, env, log)
+ selected.append({'package': name, 'version': lock['customVersion'], 'architecture': 'amd64',
+ 'file': file.name, 'sha256': digest(file)})
+ return extracted, selected
+
+
+def verify_abi(library, soname, symbols_file, work, env):
+ require(library.is_file(), 'Missing expected SONAME')
+ dynamic = capture(['readelf', '--dynamic', str(library)], work, env)
+ headers = capture(['readelf', '--program-headers', str(library)], work, env)
+ require('[' + soname + ']' in dynamic and 'GNU_RELRO' in headers and
+ ('BIND_NOW' in dynamic or re.search(r'FLAGS_1.*NOW', dynamic)), 'ABI/hardening changed')
+ required = re.findall(r'^ ([A-Za-z_][A-Za-z0-9_]*)@\S+ ',
+ symbols_file.read_text(encoding='utf-8'), re.M)
+ exports = {line.split()[-1].split('@')[0] for line in
+ capture(['nm', '--dynamic', '--defined-only', str(library)], work, env).splitlines()
+ if line.split()}
+ require(required and set(required) <= exports, 'Original Ubuntu ABI symbol missing')
+ return {'soname': soname, 'originalAbiSymbols': len(required), 'RELRO': True, 'BIND_NOW': True}
+
+
+def build(args, lock, here, verify, extra_tools):
+ release = dict(re.findall(r'^(\w+)=["\']?([^"\'\n]+)',
+ Path('/etc/os-release').read_text(encoding='utf-8'), re.M))
+ require(release.get('ID') == 'ubuntu' and release.get('VERSION_ID') == '26.04'
+ and os.geteuid() != 0, 'Use a non-root isolated Ubuntu 26.04 native builder')
+ output = args.output_dir.resolve()
+ cache = args.cache_dir.resolve() if args.cache_dir else None
+ work = args.work_dir.resolve() if args.work_dir else Path(tempfile.mkdtemp(prefix='official-backport-'))
+ COMMON.require_separate_paths(output, work, cache)
+ require(not output.exists(), 'Output directory must be new')
+ if args.work_dir: work.mkdir(parents=True, exist_ok=False)
+ home = work / 'home'
+ home.mkdir(mode=0o700)
+ jobs, job_arg = (2, '-j2') if args.jobs == 2 else (4, '-j4')
+ env = COMMON.build_environment(lock, jobs)
+ env.update(HOME=str(home), DEB_BUILD_MAINT_OPTIONS='')
+ for tool in ['dpkg-source', 'dpkg-buildpackage', 'dpkg-checkbuilddeps', 'dpkg-deb',
+ 'dpkg-query', 'patch', 'make', 'readelf', 'nm'] + extra_tools:
+ require(shutil.which(tool, path=env['PATH']), 'Native builder prerequisite missing: ' + tool)
+ require(capture(['dpkg', '--print-architecture'], work, env) == 'amd64', 'Native amd64 required')
+ print('OFFICIAL_BACKPORT_WORK_DIR=' + str(work), flush=True)
+ input_dir = work / 'inputs'
+ input_dir.mkdir()
+ if cache: cache.mkdir(parents=True, exist_ok=True)
+ for item in inputs(lock):
+ COMMON.download(item, input_dir, cache)
+ verify_inputs(lock, input_dir)
+ log = work / 'build.log'
+ source, packaging = prepare(lock, input_dir, work, env, log)
+ run(['dpkg-checkbuilddeps'], source, env, log)
+ run(['dpkg-buildpackage', '-b', '-us', '-uc', job_arg], source, env, log)
+ verify_hashes(source, packaging)
+ verify_hashes(source, lock['patchedSha256'])
+ extracted, packages = extract_packages(lock, source, work, env, log)
+ tests, evidence = verify(lock, source, extracted, work, env, log)
+ output.mkdir(parents=True)
+ for package in packages: shutil.copyfile(work / package['file'], output / package['file'])
+ lock_file = here / (lock['sourcePackage'] + '.lock.json')
+ for file, name in [(lock_file, lock_file.name), (log, 'build.log'),
+ (source / 'debian/copyright', 'COPYRIGHT')] + evidence:
+ shutil.copyfile(file, output / name)
+ for pattern in [lock['sourcePackage'] + '_*.buildinfo', lock['sourcePackage'] + '_*.changes']:
+ matches = list(work.glob(pattern))
+ require(len(matches) == 1, 'Missing/ambiguous native package receipt')
+ shutil.copyfile(matches[0], output / matches[0].name)
+ (output / 'builder-packages.tsv').write_text(capture(
+ ['dpkg-query', '--show', '--showformat=' + '$' + '{binary:Package}\t' + '$' + '{Version}\n'],
+ work, env) + '\n', encoding='utf-8')
+ provenance = {'schemaVersion': 1, 'identity': lock['identity'], 'sourceLockSha256': digest(lock_file),
+ 'sourceFiles': lock['sourceFiles'], 'patches': lock['patches'],
+ 'sharedHelpersSha256': digest(Path(__file__)),
+ 'pcre2CommonSha256': digest(HERE / 'build-pcre2-backport.py'), 'tests': tests,
+ 'packagingPreserved': packaging, 'runtimePackages': packages,
+ 'runtimeInstalled': False, 'buildCommand': ['dpkg-buildpackage', '-b', '-us', '-uc', job_arg]}
+ (output / 'provenance.json').write_text(json.dumps(provenance, indent=2) + '\n', encoding='utf-8')
+ (output / 'manifest.tsv').write_text('lock-sha256\t' + digest(lock_file) + '\n' + ''.join(
+ '\t'.join([p['package'], p['version'], p['architecture'], p['sha256'], p['file']]) + '\n'
+ for p in packages), encoding='utf-8')
+ (output / 'SHA256SUMS').write_text(''.join(digest(p) + ' ' + p.name + '\n'
+ for p in sorted(output.iterdir()) if p.is_file()), encoding='utf-8')
+
+
+def entry(here, package, version, commits, verify, extra_tools):
+ parser = argparse.ArgumentParser(description='Official security backport with preserved Ubuntu packaging')
+ 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 / (package + '.lock.json'), package, version, commits)
+ if args.validate_lock:
+ print('OFFICIAL_SOURCE_LOCK_OK ' + package)
+ return
+ require(args.output_dir is not None, '--output-dir is required')
+ build(args, lock, here, verify, extra_tools)