From ed1e5ff76f08cf059b417274f3a01b6d4b501c3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 10:50:05 +0900 Subject: [PATCH 01/21] test(ci): require authenticated gha cache transport --- .../patch-validator-image-build-cache.test.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/patch-validator-image-build-cache.test.ts b/test/patch-validator-image-build-cache.test.ts index 2f6f51334..77b5c17a1 100644 --- a/test/patch-validator-image-build-cache.test.ts +++ b/test/patch-validator-image-build-cache.test.ts @@ -8,18 +8,24 @@ const workflow = readFileSync( ); describe("patch-validator image build cache", () => { - it("reuses content-addressed BuildKit layers across successive exact PR heads", () => { - expect(workflow).toContain("docker buildx build"); - expect(workflow).toContain("--load"); + it("uses an authenticated GitHub Actions cache transport for successive exact PR heads", () => { expect(workflow).toContain( - "--cache-from=type=gha,scope=noema-patch-validator-image", + "uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294", ); + expect(workflow).toContain("load: true"); expect(workflow).toContain( - "--cache-to=type=gha,mode=max,scope=noema-patch-validator-image", + "cache-from: type=gha,scope=noema-patch-validator-image", ); - expect(workflow).not.toContain( - "timeout --signal=TERM --kill-after=30s 150m docker build \\", + expect(workflow).toContain( + "cache-to: type=gha,mode=max,scope=noema-patch-validator-image", + ); + expect(workflow).toContain("build-contexts: |"); + expect(workflow).toContain( + "validator_deps=${{ env.VALIDATOR_DEPS_CONTEXT }}", ); + expect(workflow).toContain("SOURCE_REVISION=${{ env.SOURCE_SHA }}"); + expect(workflow).toContain("tags: ${{ env.IMAGE_TAG }}"); + expect(workflow).not.toContain("docker buildx build"); }); it("seeds the shared BuildKit cache from protected main for sibling PR branches", () => { From f76571cd5faa2ed26cfb4bb84f52ca2e5643b76f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:00:14 +0900 Subject: [PATCH 02/21] fix(ci): authenticate patch-validator gha cache transport --- .github/workflows/patch-validator-image.yml | 30 ++++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index d707ee75e..923bf8954 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -185,21 +185,25 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - - name: Build exact-head patch-validator image + - name: Build exact-head patch-validator image with authenticated GitHub Actions cache + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + with: + context: . + file: Dockerfile.patch-validator + platforms: linux/amd64 + load: true + build-contexts: | + validator_deps=${{ env.VALIDATOR_DEPS_CONTEXT }} + build-args: | + SOURCE_REVISION=${{ env.SOURCE_SHA }} + tags: ${{ env.IMAGE_TAG }} + cache-from: type=gha,scope=noema-patch-validator-image + cache-to: type=gha,mode=max,scope=noema-patch-validator-image + + - name: Capture exact-head patch-validator image digest shell: bash run: | set -euo pipefail - test -d "$VALIDATOR_DEPS_CONTEXT/node_modules" - timeout --signal=TERM --kill-after=30s 150m docker buildx build \ - --load \ - --cache-from=type=gha,scope=noema-patch-validator-image \ - --cache-to=type=gha,mode=max,scope=noema-patch-validator-image \ - --build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}" \ - --platform=linux/amd64 \ - --file=Dockerfile.patch-validator \ - --build-arg=SOURCE_REVISION=${SOURCE_SHA} \ - --tag="$IMAGE_TAG" \ - . image_digest="$(docker image inspect "$IMAGE_TAG" --format '{{.Id}}')" case "$image_digest" in sha256:????????????????????????????????????????????????????????????????) ;; @@ -676,4 +680,4 @@ jobs: name: patch-validator-image-verification-${{ env.SOURCE_SHA }} path: ${{ runner.temp }}/patch-validator-evidence if-no-files-found: error - retention-days: 90 + retention-days: 90 \ No newline at end of file From 26b6e93fda0839b5f48ae83e5ac541394c8bc02a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:06:19 +0900 Subject: [PATCH 03/21] test(ci): align image build contracts with authenticated gha action --- test/patch-validator-image-build-regression.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 180db2e9c..2b158f8b0 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -28,7 +28,13 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(imageWorkflow).toContain('node-version: "24.19.0"'); expect(imageWorkflow).toContain('test "$(npm --version)" = "11.17.0"'); expect(imageWorkflow).toContain("npm ci --include=optional --ignore-scripts --no-audit --no-fund"); - expect(imageWorkflow).toContain('--build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}"'); + expect(imageWorkflow).toContain( + "uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294", + ); + expect(imageWorkflow).toContain("build-contexts: |"); + expect(imageWorkflow).toContain( + "validator_deps=${{ env.VALIDATOR_DEPS_CONTEXT }}", + ); expect(dockerfile).toContain( "COPY --from=validator_deps --chown=65532:65532 /node_modules /opt/noema/node_modules", ); @@ -114,4 +120,4 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(dockerfile).toContain("WORKDIR /usr/src/node"); expect(dockerfile).not.toContain("&& cd /usr/src/node"); }); -}); \ No newline at end of file +}); From 99ce97d0df8718b6a96b1b21bdea3b131585b933 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:06:50 +0900 Subject: [PATCH 04/21] test(ci): bind image context contract to authenticated action --- test/patch-validator-image-contract.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/patch-validator-image-contract.test.ts b/test/patch-validator-image-contract.test.ts index 10d698699..453b99d6e 100644 --- a/test/patch-validator-image-contract.test.ts +++ b/test/patch-validator-image-contract.test.ts @@ -69,7 +69,11 @@ describe("patch-validator image contract", () => { expect(imageWorkflow).toContain("node_modules/@vitest/coverage-v8/package.json"); expect(imageWorkflow).toContain("node_modules/@rolldown/binding-wasm32-wasi/package.json"); expect(imageWorkflow).toContain( - '--build-context "validator_deps=${VALIDATOR_DEPS_CONTEXT}"', + "uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294", + ); + expect(imageWorkflow).toContain("build-contexts: |"); + expect(imageWorkflow).toContain( + "validator_deps=${{ env.VALIDATOR_DEPS_CONTEXT }}", ); const runtimeStage = dockerfile.slice(dockerfile.indexOf(fromLines[1])); From 7b64714d5b1d1244256c45677acd5972c1f7f30f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:07:40 +0900 Subject: [PATCH 05/21] test(ci): preserve patch-validator contracts through gha action --- test/patch-validator-workflow.test.ts | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/patch-validator-workflow.test.ts b/test/patch-validator-workflow.test.ts index 366a2d961..cb67fc636 100644 --- a/test/patch-validator-workflow.test.ts +++ b/test/patch-validator-workflow.test.ts @@ -43,7 +43,27 @@ describe("patch-validator pull-request image verification", () => { ); expect(workflow).toContain("ref: ${{ env.SOURCE_SHA }}"); expect(workflow).toContain("timeout-minutes: 180"); - expect(workflow).toContain("timeout --signal=TERM --kill-after=30s 150m docker build"); + expect(workflow).toContain( + "uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294", + ); + expect(workflow).toContain("load: true"); + expect(workflow).toContain("context: ."); + expect(workflow).toContain("file: Dockerfile.patch-validator"); + expect(workflow).toContain("platforms: linux/amd64"); + expect(workflow).toContain("build-contexts: |"); + expect(workflow).toContain( + "validator_deps=${{ env.VALIDATOR_DEPS_CONTEXT }}", + ); + expect(workflow).toContain("build-args: |"); + expect(workflow).toContain("SOURCE_REVISION=${{ env.SOURCE_SHA }}"); + expect(workflow).toContain("tags: ${{ env.IMAGE_TAG }}"); + expect(workflow).toContain( + "cache-from: type=gha,scope=noema-patch-validator-image", + ); + expect(workflow).toContain( + "cache-to: type=gha,mode=max,scope=noema-patch-validator-image", + ); + expect(workflow).not.toContain("docker buildx build"); expect(workflow).toContain("Refuse stale pull-request head before verification"); expect(workflow).toContain("Refuse stale pull-request head after verification"); expect(workflow).toContain( @@ -71,10 +91,6 @@ describe("patch-validator pull-request image verification", () => { expect(workflow).not.toContain("sigstore/cosign-installer"); expect(workflow).not.toContain("cosign verify"); expect(workflow).not.toContain("keyless@distroless.iam.gserviceaccount.com"); - expect(workflow).toContain("docker build"); - expect(workflow).toContain("--platform=linux/amd64"); - expect(workflow).toContain("--file=Dockerfile.patch-validator"); - expect(workflow).toContain("--build-arg=SOURCE_REVISION=${SOURCE_SHA}"); expect(workflow).toContain("Verify static Node runtime identity"); expect(workflow).toContain( 'test "$(docker run --rm --pull=never --entrypoint=/nodejs/bin/node "$IMAGE_TAG" --version)" = "v24.19.0"', @@ -195,4 +211,4 @@ describe("patch-validator pull-request image verification", () => { "node scripts/verify-patch-validator-image.mjs", ); }); -}); \ No newline at end of file +}); From f6dbba3e7a877f680a2fe1d4c4c976bff5dcc1dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:20:38 +0900 Subject: [PATCH 06/21] test(ci): preserve bounded image build deadline --- ...h-validator-image-timeout-contract.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/patch-validator-image-timeout-contract.test.ts diff --git a/test/patch-validator-image-timeout-contract.test.ts b/test/patch-validator-image-timeout-contract.test.ts new file mode 100644 index 000000000..6f26031b5 --- /dev/null +++ b/test/patch-validator-image-timeout-contract.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync(".github/workflows/patch-validator-image.yml", "utf8"); + +describe("patch-validator image build timeout", () => { + it("preserves the explicit 150-minute build deadline after cache transport repair", () => { + const buildStepStart = workflow.indexOf( + "- name: Build exact-head patch-validator image with authenticated GitHub Actions cache", + ); + const digestStepStart = workflow.indexOf( + "- name: Capture exact-head patch-validator image digest", + buildStepStart, + ); + + expect(buildStepStart).toBeGreaterThanOrEqual(0); + expect(digestStepStart).toBeGreaterThan(buildStepStart); + + const buildStep = workflow.slice(buildStepStart, digestStepStart); + expect(buildStep).toContain("timeout-minutes: 150"); + }); +}); From e7be3aabefd65686d1f8def7e279577ef9e46bf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 11:35:51 +0900 Subject: [PATCH 07/21] fix(ci): preserve build step timeout with authenticated cache --- .github/workflows/patch-validator-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/patch-validator-image.yml b/.github/workflows/patch-validator-image.yml index 923bf8954..e3ea128da 100644 --- a/.github/workflows/patch-validator-image.yml +++ b/.github/workflows/patch-validator-image.yml @@ -186,6 +186,7 @@ jobs: uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Build exact-head patch-validator image with authenticated GitHub Actions cache + timeout-minutes: 150 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: context: . From eb546862d1d25bfe86410dd495a3885941c42cc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:23:09 +0900 Subject: [PATCH 08/21] fix(patch-validator): verify normalized CPE artifact versions --- ...atch-validator-static-runtime-evidence.mjs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/scripts/lib/patch-validator-static-runtime-evidence.mjs b/scripts/lib/patch-validator-static-runtime-evidence.mjs index f7bfd40b1..513d07d72 100644 --- a/scripts/lib/patch-validator-static-runtime-evidence.mjs +++ b/scripts/lib/patch-validator-static-runtime-evidence.mjs @@ -148,6 +148,33 @@ function expectedScannerSourceType(identity) { return identity.startsWith("pkg:") ? "purl" : "cpe"; } +/** + * Return the version representation Grype is expected to attach to a match + * artifact for the repository-reviewed identity. PURL scans retain the runtime + * component version verbatim. CPE scans are keyed by the normalized version in + * the exact reviewed CPE; this matters for Node's patched V8 version, where + * `process.versions.v8` carries a `-node.N` suffix but the NVD CPE does not. + */ +export function expectedScannerArtifactVersion(expectedIdentity, componentVersion) { + requireCondition( + typeof expectedIdentity === "string" && expectedIdentity.length > 0, + "embedded runtime scanner identity is invalid", + ); + requireCondition( + typeof componentVersion === "string" && componentVersion.length > 0, + "embedded runtime component version is invalid", + ); + if (expectedIdentity.startsWith("pkg:")) { + return componentVersion; + } + const fields = expectedIdentity.split(":"); + requireCondition( + fields.length === 13 && fields[0] === "cpe" && fields[1] === "2.3" && fields[5].length > 0, + "embedded runtime reviewed CPE identity is invalid", + ); + return fields[5]; +} + function verifyGrypeDatabaseEvidence(descriptor, componentKey) { const database = requireRecord( descriptor.db, @@ -229,8 +256,8 @@ function verifyEmbeddedMatchArtifact(match, component, expectedIdentity) { `embedded runtime component ${component.key} match artifact`, ); requireCondition( - artifact.version === component.version, - `embedded runtime component ${component.key} match artifact version does not match the reviewed component`, + artifact.version === expectedScannerArtifactVersion(expectedIdentity, component.version), + `embedded runtime component ${component.key} match artifact version does not match the reviewed component identity`, ); if (artifact.name != null) { requireCondition( From b1c33d1c0d3ff0dc6d3d5fae6d4d8dd1ea56270d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:23:42 +0900 Subject: [PATCH 09/21] test(patch-validator): cover normalized V8 scanner version --- ...validator-scanner-artifact-version.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/patch-validator-scanner-artifact-version.test.ts diff --git a/test/patch-validator-scanner-artifact-version.test.ts b/test/patch-validator-scanner-artifact-version.test.ts new file mode 100644 index 000000000..16aeabc1d --- /dev/null +++ b/test/patch-validator-scanner-artifact-version.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { expectedScannerArtifactVersion } from "../scripts/lib/patch-validator-static-runtime-evidence.mjs"; + +describe("patch-validator embedded scanner artifact version binding", () => { + it("uses the reviewed upstream CPE version for Node-patched V8", () => { + expect( + expectedScannerArtifactVersion( + "cpe:2.3:a:google:v8:13.6.233.17:*:*:*:*:*:*:*", + "13.6.233.17-node.51", + ), + ).toBe("13.6.233.17"); + }); + + it("keeps ordinary CPE and PURL versions exact", () => { + expect( + expectedScannerArtifactVersion( + "cpe:2.3:a:openssl:openssl:3.5.2:*:*:*:*:*:*:*", + "3.5.2", + ), + ).toBe("3.5.2"); + expect( + expectedScannerArtifactVersion("pkg:npm/undici@7.13.0", "7.13.0"), + ).toBe("7.13.0"); + }); + + it("fails closed instead of accepting an unreviewable CPE shape", () => { + expect(() => + expectedScannerArtifactVersion("cpe:2.3:a:google:v8", "13.6.233.17-node.51"), + ).toThrow(/reviewed CPE identity is invalid/i); + }); +}); From 1c91b73eb111b6e3310452fb42e144b900862c7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:48:57 +0900 Subject: [PATCH 10/21] fix(security): overlay patched c-ares in validator runtime --- Dockerfile.patch-validator | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index 0f07059c1..39b0f1df7 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -6,6 +6,8 @@ ARG NODE_VERSION=24.19.0 ARG NODE_SOURCE_SHA256=f6d95e10a0431ee1067fc6aabe9f762908b4716dd35324e1ddb4b1466b76659f ARG OPENSSL_VERSION=3.5.8 ARG OPENSSL_SOURCE_SHA256=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 +ARG CARES_VERSION=1.34.8 +ARG CARES_SOURCE_SHA256=c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78 RUN apk add --no-cache \ binutils-gold \ @@ -46,11 +48,17 @@ RUN set -eu; \ download_exact \ "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" \ "$OPENSSL_SOURCE_SHA256" \ - /tmp/openssl.tar.gz + /tmp/openssl.tar.gz; \ + download_exact \ + "https://github.com/c-ares/c-ares/releases/download/v${CARES_VERSION}/c-ares-${CARES_VERSION}.tar.gz" \ + "$CARES_SOURCE_SHA256" \ + /tmp/cares.tar.gz RUN mkdir -p /usr/src/node /usr/src/openssl \ && tar -xJf /tmp/node.tar.xz --strip-components=1 -C /usr/src/node \ - && tar -xzf /tmp/openssl.tar.gz --strip-components=1 -C /usr/src/openssl + && tar -xzf /tmp/openssl.tar.gz --strip-components=1 -C /usr/src/openssl \ + && tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares \ + && grep -Fq "#define ARES_VERSION_STR \"${CARES_VERSION}\"" /usr/src/node/deps/cares/include/ares_version.h WORKDIR /usr/src/openssl @@ -84,6 +92,7 @@ RUN ./configure \ && make install \ && test "$(/opt/node/bin/node --version)" = "v${NODE_VERSION}" \ && test "$(/opt/node/bin/npm --version)" = "11.17.0" \ + && CARES_VERSION="${CARES_VERSION}" /opt/node/bin/node --input-type=module --eval='if (process.versions.ares !== process.env.CARES_VERSION) throw new Error(`unexpected c-ares ${process.versions.ares}`)' \ && OPENSSL_VERSION="${OPENSSL_VERSION}" /opt/node/bin/node --input-type=module --eval='if (process.versions.openssl !== process.env.OPENSSL_VERSION) throw new Error(`unexpected OpenSSL ${process.versions.openssl}`)' \ && /opt/node/bin/node --input-type=module --eval='/\p{ID_Continue}/u.test("a")' \ && ! readelf -l /opt/node/bin/node | grep -q 'Requesting program interpreter' \ @@ -99,6 +108,7 @@ RUN ./configure \ && readelf -p .note.package /opt/node/bin/node \ | grep -Fq 'cpe:2.3:a:nodejs:node.js:24.19.0:*:*:*:*:*:*:*' \ && test "$(/opt/node/bin/node --version)" = "v${NODE_VERSION}" \ + && CARES_VERSION="${CARES_VERSION}" /opt/node/bin/node --input-type=module --eval='if (process.versions.ares !== process.env.CARES_VERSION) throw new Error(`unexpected c-ares ${process.versions.ares}`)' \ && OPENSSL_VERSION="${OPENSSL_VERSION}" /opt/node/bin/node --input-type=module --eval='if (process.versions.openssl !== process.env.OPENSSL_VERSION) throw new Error(`unexpected OpenSSL ${process.versions.openssl}`)' \ && /opt/node/bin/node --input-type=module --eval='/\p{ID_Continue}/u.test("a")' \ && ! readelf -l /opt/node/bin/node | grep -q 'Requesting program interpreter' \ From b79f9672373a9c38b09b98b79ad07357eeb6526e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:49:29 +0900 Subject: [PATCH 11/21] test(security): pin patched c-ares overlay --- ...patch-validator-image-build-regression.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 2b158f8b0..0089ada56 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -5,9 +5,20 @@ const dockerfile = readFileSync("Dockerfile.patch-validator", "utf8"); const imageWorkflow = readFileSync(".github/workflows/patch-validator-image.yml", "utf8"); describe("patch-validator exact-toolchain image build regression", () => { - it("builds the static runtime with the exact Node/npm toolchain declared by devEngines", () => { + it("builds the static runtime with the exact Node/npm toolchain and patched c-ares source", () => { expect(dockerfile).toContain("ARG NODE_VERSION=24.19.0"); expect(dockerfile).toContain('test "$(/opt/node/bin/npm --version)" = "11.17.0"'); + expect(dockerfile).toContain("ARG CARES_VERSION=1.34.8"); + expect(dockerfile).toContain( + "ARG CARES_SOURCE_SHA256=c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78", + ); + expect(dockerfile).toContain("c-ares-${CARES_VERSION}.tar.gz"); + expect(dockerfile).toContain( + "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares", + ); + expect(dockerfile).toContain( + "if (process.versions.ares !== process.env.CARES_VERSION) throw new Error", + ); expect(dockerfile).not.toContain("FROM validator_deps"); expect(dockerfile).not.toContain("FROM node:24.18.0-alpine3.24"); expect(dockerfile).not.toContain("--without-npm"); @@ -50,8 +61,10 @@ describe("patch-validator exact-toolchain image build regression", () => { expect(dockerfile).toContain("timeout --signal=TERM --kill-after=30s 5m"); expect(dockerfile).toContain("node-v${NODE_VERSION}.tar.xz"); expect(dockerfile).toContain("openssl-${OPENSSL_VERSION}.tar.gz"); + expect(dockerfile).toContain("c-ares-${CARES_VERSION}.tar.gz"); expect(dockerfile).toContain("NODE_SOURCE_SHA256"); expect(dockerfile).toContain("OPENSSL_SOURCE_SHA256"); + expect(dockerfile).toContain("CARES_SOURCE_SHA256"); expect(dockerfile).toContain("sha256sum"); expect(dockerfile).not.toContain("ADD --checksum"); }); From d8295355afa684ef0dd185038aa59b0312ecebca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:50:58 +0900 Subject: [PATCH 12/21] fix(security): review exact embedded runtime applicability --- ...lidator-embedded-runtime-applicability.mjs | 130 ++++++++++++++---- 1 file changed, 103 insertions(+), 27 deletions(-) diff --git a/scripts/lib/patch-validator-embedded-runtime-applicability.mjs b/scripts/lib/patch-validator-embedded-runtime-applicability.mjs index 094e3ae14..79686f16d 100644 --- a/scripts/lib/patch-validator-embedded-runtime-applicability.mjs +++ b/scripts/lib/patch-validator-embedded-runtime-applicability.mjs @@ -5,14 +5,27 @@ const LEGACY_V8_CVES = new Set([ "CVE-2011-5037", "CVE-2011-3886", ]); +const V8_ARRAY_SORT_CVES = new Set(["CVE-2026-85046"]); +const SQLITE_FIXED_RANGE_FINDINGS = new Set([ + "BIT-sqlite-2024-0232", + "BIT-sqlite-2025-29088", + "BIT-sqlite-2025-6965", +]); +const ZLIB_RUBYGEM_FINDINGS = new Set(["GHSA-g857-hhfv-j68w"]); const QUIC_DISABLED_REASON = "QUIC transport dependency disabled in this build"; const HTTP3_DISABLED_REASON = "HTTP/3 dependency disabled in this build"; const OPENSSL_NON_APPLICABLE_REASON = "Node runtime proves QUIC and HTTP/3 dependencies are disabled"; const NGHTTP2_NON_APPLICABLE_REASON = "CVE affects the nghttpx proxy, not Node's embedded libnghttp2 runtime"; -const V8_NON_APPLICABLE_REASON = +const LEGACY_V8_NON_APPLICABLE_REASON = "Exact Node 24.19.0 V8 runtime is newer than the reviewed affected legacy V8 releases"; +const V8_ARRAY_SORT_NON_APPLICABLE_REASON = + "Exact Node 24.19.0 V8 branch lacks the vulnerable inlined Array.prototype.sort reducers"; +const SQLITE_NON_APPLICABLE_REASON = + "Exact SQLite 3.53.3 runtime is newer than the reviewed affected SQLite ranges"; +const ZLIB_NON_APPLICABLE_REASON = + "Advisory applies to the Ruby zlib gem GzipReader wrapper, not Node's embedded C zlib runtime"; const EXPECTED_NODE_VERSION = "24.19.0"; const EXPECTED_NGHTTP2_VERSION = "1.69.0"; const EXPECTED_NGHTTP2_CPE = @@ -21,6 +34,12 @@ const EXPECTED_V8_VERSION = "13.6.233.17-node.51"; const EXPECTED_V8_SCANNER_VERSION = "13.6.233.17"; const EXPECTED_V8_CPE = "cpe:2.3:a:google:v8:13.6.233.17:*:*:*:*:*:*:*"; +const EXPECTED_SQLITE_VERSION = "3.53.3"; +const EXPECTED_SQLITE_CPE = + "cpe:2.3:a:sqlite:sqlite:3.53.3:*:*:*:*:*:*:*"; +const EXPECTED_ZLIB_VERSION = "1.3.2.1-motley-3246f1b"; +const EXPECTED_ZLIB_CPE = + "cpe:2.3:a:zlib:zlib:1.3.2.1-motley-3246f1b:*:*:*:*:*:*:*"; function isRecord(value) { return Object.prototype.toString.call(value) === "[object Object]"; @@ -113,20 +132,6 @@ function matchUsesExactReviewedCpe( }); } -function scanMatchesExactComponent(rawComponentScan, component) { - return ( - isRecord(rawComponentScan) - && rawComponentScan.key === component.key - && rawComponentScan.identity === component.cpe - && isRecord(rawComponentScan.scanner_output) - && rawComponentScan.scanner_output.source?.type === "cpe" - && rawComponentScan.scanner_output.source?.target === component.cpe - && rawComponentScan.scanner_output.descriptor?.name === "grype" - && rawComponentScan.scanner_output.descriptor?.version === "0.116.1" - && Array.isArray(rawComponentScan.scanner_output.matches) - ); -} - function filterReviewedMatches( rawComponentScan, component, @@ -181,23 +186,43 @@ function filterReviewedMatches( }; } +function scanMatchesExactComponent(rawComponentScan, component) { + return ( + isRecord(rawComponentScan) + && rawComponentScan.key === component.key + && rawComponentScan.identity === component.cpe + && isRecord(rawComponentScan.scanner_output) + && rawComponentScan.scanner_output.source?.type === "cpe" + && rawComponentScan.scanner_output.source?.target === component.cpe + && rawComponentScan.scanner_output.descriptor?.name === "grype" + && rawComponentScan.scanner_output.descriptor?.version === "0.116.1" + && Array.isArray(rawComponentScan.scanner_output.matches) + ); +} + /** * Apply narrowly reviewed applicability evidence without mutating the retained - * raw scanner receipt. Exceptions are exact-CVE, exact-component, exact-CPE, - * Grype-0.116.1 NVD-CPE matches only: + * raw scanner receipt. Exceptions are exact-finding, exact-component, + * exact-CPE, Grype-0.116.1 NVD-CPE matches only: * * - OpenSSL CVE-2026-14456 requires exact evidence that both QUIC transport * dependencies are disabled in this Node build. * - nghttp2 CVE-2026-58055 describes the nghttpx proxy request-forwarding * behavior; the exact Node 24.19.0 runtime inventories libnghttp2 as a * statically bundled dependency, not the nghttpx proxy executable. - * - the three legacy V8 advisories are bounded to historical affected V8/Node - * releases that predate the exact Node 24.19.0 / V8 13.6.233.17-node.51 - * runtime retained in the inventory. Grype reports the normalized CPE - * artifact version 13.6.233.17, which is accepted only for this exact - * reviewed runtime/CPE pairing. + * - legacy V8 advisories are bounded to historical affected V8/Node releases + * that predate the exact Node 24.19.0 / V8 13.6.233.17-node.51 runtime. + * - CVE-2026-85046 is tied to V8's inlined Array.prototype.sort reducers. The + * exact signed Node 24.19.0 V8 branch contains neither affected reducer, so + * the generic V8 CPE range is not execution-path evidence for this runtime. + * - the three SQLite findings have reviewed vulnerable ranges ending no later + * than 3.50.1, while this exact embedded runtime is SQLite 3.53.3. + * - GHSA-g857-hhfv-j68w is a RubyGems zlib/GzipReader advisory, not an + * upstream C zlib-library advisory; the exact Node component is the latter. * - * Every other scanner match remains untouched for the strict verifier. + * Grype reports the normalized V8 CPE artifact version 13.6.233.17, which is + * accepted only for the exact reviewed runtime/CPE pairing. Every other + * scanner match remains untouched for the strict verifier. */ export function applyReviewedEmbeddedRuntimeApplicability({ inventory, scan }) { const nonApplicableMatches = []; @@ -234,6 +259,26 @@ export function applyReviewedEmbeddedRuntimeApplicability({ inventory, scan }) { EXPECTED_V8_CPE, ) : null; + const sqlite = exactNodeRuntime + && inventory.process_versions.sqlite === EXPECTED_SQLITE_VERSION + ? exactBundledCpeComponent( + inventory, + "sqlite", + "sqlite", + EXPECTED_SQLITE_VERSION, + EXPECTED_SQLITE_CPE, + ) + : null; + const zlib = exactNodeRuntime + && inventory.process_versions.zlib === EXPECTED_ZLIB_VERSION + ? exactBundledCpeComponent( + inventory, + "zlib", + "zlib", + EXPECTED_ZLIB_VERSION, + EXPECTED_ZLIB_CPE, + ) + : null; let changed = false; const reviewedComponents = scan.components.map((rawComponentScan) => { @@ -259,16 +304,47 @@ export function applyReviewedEmbeddedRuntimeApplicability({ inventory, scan }) { current = nghttp2Result.componentScan; changed ||= nghttp2Result.changed; - const v8Result = filterReviewedMatches( + const legacyV8Result = filterReviewedMatches( current, v8, LEGACY_V8_CVES, - V8_NON_APPLICABLE_REASON, + LEGACY_V8_NON_APPLICABLE_REASON, nonApplicableMatches, EXPECTED_V8_SCANNER_VERSION, ); - current = v8Result.componentScan; - changed ||= v8Result.changed; + current = legacyV8Result.componentScan; + changed ||= legacyV8Result.changed; + + const arraySortV8Result = filterReviewedMatches( + current, + v8, + V8_ARRAY_SORT_CVES, + V8_ARRAY_SORT_NON_APPLICABLE_REASON, + nonApplicableMatches, + EXPECTED_V8_SCANNER_VERSION, + ); + current = arraySortV8Result.componentScan; + changed ||= arraySortV8Result.changed; + + const sqliteResult = filterReviewedMatches( + current, + sqlite, + SQLITE_FIXED_RANGE_FINDINGS, + SQLITE_NON_APPLICABLE_REASON, + nonApplicableMatches, + ); + current = sqliteResult.componentScan; + changed ||= sqliteResult.changed; + + const zlibResult = filterReviewedMatches( + current, + zlib, + ZLIB_RUBYGEM_FINDINGS, + ZLIB_NON_APPLICABLE_REASON, + nonApplicableMatches, + ); + current = zlibResult.componentScan; + changed ||= zlibResult.changed; return current; }); From 1175d77ee75552d153ccae3b28ba67d6bedc6953 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:51:34 +0900 Subject: [PATCH 13/21] test(security): cover reviewed runtime false positives --- ...h-validator-reviewed-applicability.test.ts | 133 +++++++++++++++++- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/test/patch-validator-reviewed-applicability.test.ts b/test/patch-validator-reviewed-applicability.test.ts index 12dedf896..f5ceb6ce7 100644 --- a/test/patch-validator-reviewed-applicability.test.ts +++ b/test/patch-validator-reviewed-applicability.test.ts @@ -4,6 +4,8 @@ import { applyReviewedEmbeddedRuntimeApplicability } from "../scripts/lib/patch- const nghttp2Cpe = "cpe:2.3:a:nghttp2:nghttp2:1.69.0:*:*:*:*:*:*:*"; const v8Cpe = "cpe:2.3:a:google:v8:13.6.233.17:*:*:*:*:*:*:*"; +const sqliteCpe = "cpe:2.3:a:sqlite:sqlite:3.53.3:*:*:*:*:*:*:*"; +const zlibCpe = "cpe:2.3:a:zlib:zlib:1.3.2.1-motley-3246f1b:*:*:*:*:*:*:*"; function componentScan( key: string, @@ -47,7 +49,9 @@ function inventory() { process_versions: { node: "24.19.0", nghttp2: "1.69.0", + sqlite: "3.53.3", v8: "13.6.233.17-node.51", + zlib: "1.3.2.1-motley-3246f1b", }, components: [ { @@ -57,6 +61,13 @@ function inventory() { classification: "bundled_dependency", cpe: nghttp2Cpe, }, + { + key: "sqlite", + name: "sqlite", + version: "3.53.3", + classification: "bundled_dependency", + cpe: sqliteCpe, + }, { key: "v8", name: "v8", @@ -64,6 +75,13 @@ function inventory() { classification: "bundled_dependency", cpe: v8Cpe, }, + { + key: "zlib", + name: "zlib", + version: "1.3.2.1-motley-3246f1b", + classification: "bundled_dependency", + cpe: zlibCpe, + }, ], }; } @@ -128,16 +146,89 @@ describe("reviewed embedded-runtime applicability", () => { }, ); - it("does not convert a different nghttp2 advisory into an applicability exception", () => { + it("marks CVE-2026-85046 non-applicable only to the exact Node 24.19.0 V8 branch/CPE", () => { const scan = { components: [ componentScan( - "nghttp2", - "nghttp2", - "1.69.0", - "1.69.0", - nghttp2Cpe, - "CVE-2099-4242", + "v8", + "v8", + "13.6.233.17-node.51", + "13.6.233.17", + v8Cpe, + "CVE-2026-85046", + "High", + ), + ], + }; + + const reviewed = applyReviewedEmbeddedRuntimeApplicability({ + inventory: inventory(), + scan, + }); + + expect(reviewed.scan.components[0].scanner_output.matches).toEqual([]); + expect(reviewed.nonApplicableMatches).toContainEqual({ + component_key: "v8", + vulnerability_id: "CVE-2026-85046", + reason: "Exact Node 24.19.0 V8 branch lacks the vulnerable inlined Array.prototype.sort reducers", + }); + + const differentNode = inventory(); + differentNode.node_version = "24.19.1"; + differentNode.process_versions.node = "24.19.1"; + const retained = applyReviewedEmbeddedRuntimeApplicability({ + inventory: differentNode, + scan, + }); + expect(retained.scan).toBe(scan); + expect(retained.nonApplicableMatches).toEqual([]); + }); + + it.each([ + "BIT-sqlite-2024-0232", + "BIT-sqlite-2025-29088", + "BIT-sqlite-2025-6965", + ])( + "marks reviewed fixed-range SQLite finding %s non-applicable to exact SQLite 3.53.3", + (vulnerabilityId) => { + const scan = { + components: [ + componentScan( + "sqlite", + "sqlite", + "3.53.3", + "3.53.3", + sqliteCpe, + vulnerabilityId, + "High", + ), + ], + }; + + const reviewed = applyReviewedEmbeddedRuntimeApplicability({ + inventory: inventory(), + scan, + }); + + expect(reviewed.scan.components[0].scanner_output.matches).toEqual([]); + expect(reviewed.nonApplicableMatches).toContainEqual({ + component_key: "sqlite", + vulnerability_id: vulnerabilityId, + reason: "Exact SQLite 3.53.3 runtime is newer than the reviewed affected SQLite ranges", + }); + }, + ); + + it("rejects the RubyGems zlib advisory as a product mismatch for Node's embedded C zlib", () => { + const scan = { + components: [ + componentScan( + "zlib", + "zlib", + "1.3.2.1-motley-3246f1b", + "1.3.2.1-motley-3246f1b", + zlibCpe, + "GHSA-g857-hhfv-j68w", "Medium", ), ], @@ -148,6 +239,34 @@ describe("reviewed embedded-runtime applicability", () => { scan, }); + expect(reviewed.scan.components[0].scanner_output.matches).toEqual([]); + expect(reviewed.nonApplicableMatches).toContainEqual({ + component_key: "zlib", + vulnerability_id: "GHSA-g857-hhfv-j68w", + reason: "Advisory applies to the Ruby zlib gem GzipReader wrapper, not Node's embedded C zlib runtime", + }); + }); + + it("does not convert an unreviewed advisory into an applicability exception", () => { + const scan = { + components: [ + componentScan( + "sqlite", + "sqlite", + "3.53.3", + "3.53.3", + sqliteCpe, + "CVE-2099-4242", + "High", + ), + ], + }; + + const reviewed = applyReviewedEmbeddedRuntimeApplicability({ + inventory: inventory(), + scan, + }); + expect(reviewed.scan).toBe(scan); expect(reviewed.nonApplicableMatches).toEqual([]); }); From 3fc00d71ee9ad83c69feed1cf7c6abf04856f555 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 17:52:19 +0900 Subject: [PATCH 14/21] docs(security): doctor embedded runtime applicability review --- ...bedded-runtime-applicability-2026-09-09.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/doctoring/patch-validator-embedded-runtime-applicability-2026-09-09.md diff --git a/docs/doctoring/patch-validator-embedded-runtime-applicability-2026-09-09.md b/docs/doctoring/patch-validator-embedded-runtime-applicability-2026-09-09.md new file mode 100644 index 000000000..2fb01b3f4 --- /dev/null +++ b/docs/doctoring/patch-validator-embedded-runtime-applicability-2026-09-09.md @@ -0,0 +1,69 @@ +# Patch-validator embedded-runtime applicability review — 2026-09-09 + +## Problem and exact evidence + +Noema patch-validator image run `34328891694` on exact source `b1c33d1c0d3ff0dc6d3d5fae6d4d8dd1ea56270d` completed authenticated BuildKit cache import/hit/export, exact image build, static-runtime inspection, isolated smoke execution, SBOM generation, and raw binary/embedded-runtime scans before the final exact-receipt verifier failed. The retained verification artifact is `10095371779` with artifact digest `sha256:21d9d7a7403ae4be1af21b4a769baa23d77714b2239cbc4c7975ac498728c7b4`. + +The raw Grype receipt contained five distinct classes that must not be handled as one generic scanner exception: + +- c-ares `1.34.6` → `CVE-2026-33630`; +- SQLite `3.53.3` → `BIT-sqlite-2024-0232`, `BIT-sqlite-2025-29088`, and `BIT-sqlite-2025-6965`; +- V8 `13.6.233.17-node.51`, normalized to reviewed CPE version `13.6.233.17` → `CVE-2026-85046` plus three already reviewed legacy advisories; +- zlib `1.3.2.1-motley-3246f1b` → `GHSA-g857-hhfv-j68w`; +- nghttp2 `1.69.0` → the already reviewed nghttpx-only `CVE-2026-58055`. + +Raw scanner evidence remains retained. Applicability review creates a derived receipt and is permitted only when exact component identity, CPE provenance, scanner identity, finding ID, and independently reviewed evidence all agree. + +## Decisions + +### c-ares: repair the vulnerable component; do not suppress the finding + +Upstream c-ares 1.34.7 is explicitly a security release fixing `CVE-2026-33630`, a remotely triggerable use-after-free/double-free in `ares_getaddrinfo()` over TCP. The exact validator runtime contains c-ares 1.34.6, so the finding is applicable. c-ares 1.34.8 retains the security fix and restores API compatibility after 1.34.7's unintended callback-signature break. + +The validator therefore keeps Node 24.19.0 as its reviewed executable identity but overlays the official c-ares 1.34.8 release source into Node's vendored `deps/cares` tree before compilation. The release tarball is authenticated by SHA-256 `c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78`, and the built runtime must report `process.versions.ares === "1.34.8"`. This avoids converting an actually vulnerable component into an applicability exception. + +Rejected alternatives: ignoring the finding because the smoke container runs with `--network none`; weakening the severity threshold; or accepting c-ares 1.34.6 based on elapsed isolation evidence. Those alternatives make deployment configuration part of binary vulnerability truth and are not accepted. + +### SQLite: exact runtime is outside all three reviewed vulnerable ranges + +The embedded runtime is SQLite 3.53.3. Reviewed records bound the findings as follows: + +- `CVE-2024-0232` / `BIT-sqlite-2024-0232`: affected SQLite 3.43.0 through 3.43.1; fixed in 3.43.2. +- `CVE-2025-29088` / `BIT-sqlite-2025-29088`: SQLite 3.49.0 before 3.49.1. +- `CVE-2025-6965` / `BIT-sqlite-2025-6965`: SQLite versions before 3.50.2. + +For this exact 3.53.3 component/CPE, those scanner matches are version-range false positives. The derived applicability receipt may remove only those three exact finding IDs and only when the component remains exactly SQLite 3.53.3 with the reviewed CPE and NVD-CPE Grype provenance. Any other SQLite finding remains blocking. + +### zlib: scanner matched an advisory for a different product/ecosystem + +`GHSA-g857-hhfv-j68w` / `CVE-2026-27820` is published by `ruby/zlib` for the RubyGems package `zlib`; the affected code is Ruby's `Zlib::GzipReader` wrapper. Noema's runtime component is Node's embedded upstream C zlib library and is represented by a zlib CPE, not a RubyGems PURL. The derived applicability receipt may remove only this exact GHSA when the exact Node zlib component/CPE and NVD-CPE Grype provenance match. Other zlib findings remain blocking. + +### V8 CVE-2026-85046: exact Node branch lacks the vulnerable implementation path + +The primary V8 fix for Chromium issue 542403045 changes `JSCallReducer::ReduceArraySort` and `MaglevGraphBuilder::TryReduceArrayPrototypeSort` so mixed receiver element kinds are not used by the inlined `Array.prototype.sort` path. The exact signed Node.js v24.19.0 release commit is `cdc1b38d40cb567b7ad0b39c86addf830a0af0ae`. Inspection of that release's vendored V8 `deps/v8/src/compiler/js-call-reducer.cc` and `deps/v8/src/maglev/maglev-graph-builder.cc` finds neither affected reducer. Thus the generic V8 CPE range is not sufficient evidence that Node 24.19.0's `13.6.233.17-node.51` branch contains the vulnerable path. + +The derived applicability receipt may remove `CVE-2026-85046` only for exact Node 24.19.0, exact V8 `13.6.233.17-node.51`, reviewed CPE `cpe:2.3:a:google:v8:13.6.233.17:*:*:*:*:*:*:*`, normalized scanner artifact version `13.6.233.17`, Grype 0.116.1, and NVD-CPE provenance. A Node/V8/CPE/scanner/finding mismatch must fail closed. + +## Risk and follow-up + +Applicability review is deliberately exact-version rather than an open-ended semantic-version rule. A future Node, V8, SQLite, zlib, scanner, or CPE change loses the exception automatically and must be reviewed again. c-ares remains a source repair, not an exception. The image lane must rebuild from the new exact head and prove the patched c-ares version, raw scan retention, derived reviewed receipt, strict final verifier, isolation smoke, SBOM, and unchanged image/source binding before integration. + +Noema does not take ownership of scanner authority, quarantine policy, outbound control, or provider routing through this review. It consumes scanner evidence and applies only its own exact runtime applicability decision at the validator acceptance boundary. + +## Traceability / references + +c-ares. (2026, July 6). *c-ares version 1.34.7 — security release*. https://c-ares.org/changelog.html + +c-ares. (2026, July 7). *c-ares version 1.34.8*. https://github.com/c-ares/c-ares/releases/tag/v1.34.8 + +GitHub. (2026, April 16). *Buffer overflow vulnerability in Zlib::GzipReader (GHSA-g857-hhfv-j68w)*. https://github.com/ruby/zlib/security/advisories/GHSA-g857-hhfv-j68w + +National Institute of Standards and Technology. (2024, January 16; modified 2026, June 17). *CVE-2024-0232*. National Vulnerability Database. https://nvd.nist.gov/vuln/detail/CVE-2024-0232 + +Open Source Vulnerabilities. (2025). *BIT-sqlite-2025-29088*. https://osv.dev/vulnerability/BIT-sqlite-2025-29088 + +Open Source Vulnerabilities. (2025; modified 2026). *BIT-sqlite-2025-6965*. https://osv.dev/vulnerability/BIT-sqlite-2025-6965 + +V8 Project. (2026, August 7). *[compiler] Don't inline Array.prototype.sort on mixed elements kinds* (commit `e0562d87ad9c17042b581582c99237d798572e67`). https://github.com/v8/v8/commit/e0562d87ad9c17042b581582c99237d798572e67 + +Node.js. (2026, August 3). *Node.js v24.19.0 Krypton (LTS) release* (signed tag; commit `cdc1b38d40cb567b7ad0b39c86addf830a0af0ae`). https://github.com/nodejs/node/releases/tag/v24.19.0 From 6c84bc0dabc948241d593ae57514e12984b75626 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 18:03:44 +0900 Subject: [PATCH 15/21] test(security): reject mixed c-ares source overlays --- ...validator-cares-source-replacement.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/patch-validator-cares-source-replacement.test.ts diff --git a/test/patch-validator-cares-source-replacement.test.ts b/test/patch-validator-cares-source-replacement.test.ts new file mode 100644 index 000000000..0bc3ea915 --- /dev/null +++ b/test/patch-validator-cares-source-replacement.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const dockerfile = readFileSync("Dockerfile.patch-validator", "utf8"); + +describe("patch-validator c-ares source replacement", () => { + it("removes the vendored Node c-ares tree before extracting the reviewed replacement", () => { + const removeVendoredTree = "rm -rf /usr/src/node/deps/cares"; + const createReplacementTree = "mkdir -p /usr/src/node/deps/cares"; + const extractReviewedTree = + "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares"; + + expect(dockerfile).toContain(removeVendoredTree); + expect(dockerfile).toContain(createReplacementTree); + expect(dockerfile).toContain(extractReviewedTree); + expect(dockerfile.indexOf(removeVendoredTree)).toBeLessThan( + dockerfile.indexOf(extractReviewedTree), + ); + expect(dockerfile.indexOf(createReplacementTree)).toBeLessThan( + dockerfile.indexOf(extractReviewedTree), + ); + }); +}); From 050ab45ae9b28a1f8f043e3a62d3b97c86c24d73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 18:04:36 +0900 Subject: [PATCH 16/21] fix(security): replace vendored c-ares tree atomically --- Dockerfile.patch-validator | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index 39b0f1df7..a1cf143b5 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -57,6 +57,8 @@ RUN set -eu; \ RUN mkdir -p /usr/src/node /usr/src/openssl \ && tar -xJf /tmp/node.tar.xz --strip-components=1 -C /usr/src/node \ && tar -xzf /tmp/openssl.tar.gz --strip-components=1 -C /usr/src/openssl \ + && rm -rf /usr/src/node/deps/cares \ + && mkdir -p /usr/src/node/deps/cares \ && tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares \ && grep -Fq "#define ARES_VERSION_STR \"${CARES_VERSION}\"" /usr/src/node/deps/cares/include/ares_version.h @@ -141,4 +143,4 @@ COPY --chown=65532:65532 patch-validator/runtime.mjs /opt/noema/runtime.mjs COPY --chown=65532:65532 patch-validator/validator-tsconfig.json /opt/noema/validator-tsconfig.json COPY --chown=65532:65532 patch-validator/validator-vitest.config.mjs /opt/noema/validator-vitest.config.mjs -ENTRYPOINT ["/nodejs/bin/node", "--input-type=module", "--eval", "import { runCli } from '/opt/noema/runtime.mjs'; import { runEntrypoint } from '/opt/noema/entrypoint.mjs'; process.exitCode = runEntrypoint({ runCliImpl: runCli, writeDiagnostic: (message) => process.stderr.write(message) });"] \ No newline at end of file +ENTRYPOINT ["/nodejs/bin/node", "--input-type=module", "--eval", "import { runCli } from '/opt/noema/runtime.mjs'; import { runEntrypoint } from '/opt/noema/entrypoint.mjs'; process.exitCode = runEntrypoint({ runCliImpl: runCli, writeDiagnostic: (message) => process.stderr.write(message) });"] From 858f95e5bd9958043103557cd0303bc0158ef2c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 20:05:13 +0900 Subject: [PATCH 17/21] test(security): preserve Node c-ares integration glue --- ...validator-cares-source-replacement.test.ts | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/test/patch-validator-cares-source-replacement.test.ts b/test/patch-validator-cares-source-replacement.test.ts index 0bc3ea915..303b8b66e 100644 --- a/test/patch-validator-cares-source-replacement.test.ts +++ b/test/patch-validator-cares-source-replacement.test.ts @@ -4,20 +4,45 @@ import { describe, expect, it } from "vitest"; const dockerfile = readFileSync("Dockerfile.patch-validator", "utf8"); describe("patch-validator c-ares source replacement", () => { - it("removes the vendored Node c-ares tree before extracting the reviewed replacement", () => { - const removeVendoredTree = "rm -rf /usr/src/node/deps/cares"; - const createReplacementTree = "mkdir -p /usr/src/node/deps/cares"; + it("replaces reviewed upstream sources atomically while preserving Node-owned integration glue", () => { + const preserveConfig = + "cp -R /usr/src/node/deps/cares/config /tmp/node-cares-integration/config"; + const preserveGitignore = + "cp /usr/src/node/deps/cares/.gitignore /tmp/node-cares-integration/.gitignore"; + const preserveGyp = + "cp /usr/src/node/deps/cares/cares.gyp /tmp/node-cares-integration/cares.gyp"; + const preserveGn = + "cp /usr/src/node/deps/cares/*.gn /usr/src/node/deps/cares/*.gni /tmp/node-cares-integration/"; const extractReviewedTree = - "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares"; + "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /tmp/cares-reviewed"; + const removeReviewedTests = "rm -rf /tmp/cares-reviewed/test"; + const removeVendoredTree = "rm -rf /usr/src/node/deps/cares"; + const installReviewedTree = "mv /tmp/cares-reviewed /usr/src/node/deps/cares"; + + for (const required of [ + preserveConfig, + preserveGitignore, + preserveGyp, + preserveGn, + extractReviewedTree, + removeReviewedTests, + removeVendoredTree, + installReviewedTree, + ]) { + expect(dockerfile).toContain(required); + } - expect(dockerfile).toContain(removeVendoredTree); - expect(dockerfile).toContain(createReplacementTree); - expect(dockerfile).toContain(extractReviewedTree); + expect(dockerfile.indexOf(preserveGyp)).toBeLessThan( + dockerfile.indexOf(removeVendoredTree), + ); + expect(dockerfile.indexOf(extractReviewedTree)).toBeLessThan( + dockerfile.indexOf(removeVendoredTree), + ); expect(dockerfile.indexOf(removeVendoredTree)).toBeLessThan( - dockerfile.indexOf(extractReviewedTree), + dockerfile.indexOf(installReviewedTree), ); - expect(dockerfile.indexOf(createReplacementTree)).toBeLessThan( - dockerfile.indexOf(extractReviewedTree), + expect(dockerfile).not.toContain( + "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares", ); }); }); From 4d1797ed5d9aa08c319d7aa23adef93a61f85367 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 20:05:57 +0900 Subject: [PATCH 18/21] fix(security): preserve Node c-ares integration on source replacement --- Dockerfile.patch-validator | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index a1cf143b5..49ea32e58 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -54,12 +54,25 @@ RUN set -eu; \ "$CARES_SOURCE_SHA256" \ /tmp/cares.tar.gz +# Node's canonical c-ares updater replaces the upstream payload while retaining +# Node-owned config/GYP/GN integration. Preserve only that build glue; the +# reviewed c-ares source tree itself is replaced atomically. RUN mkdir -p /usr/src/node /usr/src/openssl \ && tar -xJf /tmp/node.tar.xz --strip-components=1 -C /usr/src/node \ && tar -xzf /tmp/openssl.tar.gz --strip-components=1 -C /usr/src/openssl \ + && mkdir -p /tmp/node-cares-integration /tmp/cares-reviewed \ + && cp -R /usr/src/node/deps/cares/config /tmp/node-cares-integration/config \ + && cp /usr/src/node/deps/cares/.gitignore /tmp/node-cares-integration/.gitignore \ + && cp /usr/src/node/deps/cares/cares.gyp /tmp/node-cares-integration/cares.gyp \ + && cp /usr/src/node/deps/cares/*.gn /usr/src/node/deps/cares/*.gni /tmp/node-cares-integration/ \ + && tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /tmp/cares-reviewed \ + && rm -rf /tmp/cares-reviewed/test \ + && cp -R /tmp/node-cares-integration/config /tmp/cares-reviewed/config \ + && cp /tmp/node-cares-integration/.gitignore /tmp/cares-reviewed/.gitignore \ + && cp /tmp/node-cares-integration/cares.gyp /tmp/cares-reviewed/cares.gyp \ + && cp /tmp/node-cares-integration/*.gn /tmp/node-cares-integration/*.gni /tmp/cares-reviewed/ \ && rm -rf /usr/src/node/deps/cares \ - && mkdir -p /usr/src/node/deps/cares \ - && tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares \ + && mv /tmp/cares-reviewed /usr/src/node/deps/cares \ && grep -Fq "#define ARES_VERSION_STR \"${CARES_VERSION}\"" /usr/src/node/deps/cares/include/ares_version.h WORKDIR /usr/src/openssl From 19f74d217a19af03e95f8ee5eebff45e5defa549 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 20:10:09 +0900 Subject: [PATCH 19/21] test(security): align c-ares regression with atomic replacement --- test/patch-validator-image-build-regression.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/patch-validator-image-build-regression.test.ts b/test/patch-validator-image-build-regression.test.ts index 0089ada56..89570bfda 100644 --- a/test/patch-validator-image-build-regression.test.ts +++ b/test/patch-validator-image-build-regression.test.ts @@ -14,7 +14,7 @@ describe("patch-validator exact-toolchain image build regression", () => { ); expect(dockerfile).toContain("c-ares-${CARES_VERSION}.tar.gz"); expect(dockerfile).toContain( - "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares", + "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /tmp/cares-reviewed", ); expect(dockerfile).toContain( "if (process.versions.ares !== process.env.CARES_VERSION) throw new Error", From e8fb138d2b1167546d0d444cac40ce2635e0194e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 20:39:21 +0900 Subject: [PATCH 20/21] test(ci): require canonical c-ares config replacement --- ...h-validator-cares-source-replacement.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/patch-validator-cares-source-replacement.test.ts b/test/patch-validator-cares-source-replacement.test.ts index 303b8b66e..c0ca75834 100644 --- a/test/patch-validator-cares-source-replacement.test.ts +++ b/test/patch-validator-cares-source-replacement.test.ts @@ -16,8 +16,13 @@ describe("patch-validator c-ares source replacement", () => { const extractReviewedTree = "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /tmp/cares-reviewed"; const removeReviewedTests = "rm -rf /tmp/cares-reviewed/test"; + const replaceReviewedConfig = "rm -rf /tmp/cares-reviewed/config"; + const restoreNodeConfig = + "cp -R /tmp/node-cares-integration/config /tmp/cares-reviewed/config"; const removeVendoredTree = "rm -rf /usr/src/node/deps/cares"; const installReviewedTree = "mv /tmp/cares-reviewed /usr/src/node/deps/cares"; + const assertLinuxConfig = + "test -f /usr/src/node/deps/cares/config/linux/ares_config.h"; for (const required of [ preserveConfig, @@ -26,12 +31,21 @@ describe("patch-validator c-ares source replacement", () => { preserveGn, extractReviewedTree, removeReviewedTests, + replaceReviewedConfig, + restoreNodeConfig, removeVendoredTree, installReviewedTree, + assertLinuxConfig, ]) { expect(dockerfile).toContain(required); } + expect(dockerfile.indexOf(extractReviewedTree)).toBeLessThan( + dockerfile.indexOf(replaceReviewedConfig), + ); + expect(dockerfile.indexOf(replaceReviewedConfig)).toBeLessThan( + dockerfile.indexOf(restoreNodeConfig), + ); expect(dockerfile.indexOf(preserveGyp)).toBeLessThan( dockerfile.indexOf(removeVendoredTree), ); @@ -41,6 +55,9 @@ describe("patch-validator c-ares source replacement", () => { expect(dockerfile.indexOf(removeVendoredTree)).toBeLessThan( dockerfile.indexOf(installReviewedTree), ); + expect(dockerfile.indexOf(installReviewedTree)).toBeLessThan( + dockerfile.indexOf(assertLinuxConfig), + ); expect(dockerfile).not.toContain( "tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /usr/src/node/deps/cares", ); From 772bfba546d75ed2495d57ce43d3d390230b85fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 20:40:05 +0900 Subject: [PATCH 21/21] fix(ci): restore Node-owned c-ares config atomically --- Dockerfile.patch-validator | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile.patch-validator b/Dockerfile.patch-validator index 49ea32e58..caf1ab9e1 100644 --- a/Dockerfile.patch-validator +++ b/Dockerfile.patch-validator @@ -67,12 +67,14 @@ RUN mkdir -p /usr/src/node /usr/src/openssl \ && cp /usr/src/node/deps/cares/*.gn /usr/src/node/deps/cares/*.gni /tmp/node-cares-integration/ \ && tar -xzf /tmp/cares.tar.gz --strip-components=1 -C /tmp/cares-reviewed \ && rm -rf /tmp/cares-reviewed/test \ + && rm -rf /tmp/cares-reviewed/config \ && cp -R /tmp/node-cares-integration/config /tmp/cares-reviewed/config \ && cp /tmp/node-cares-integration/.gitignore /tmp/cares-reviewed/.gitignore \ && cp /tmp/node-cares-integration/cares.gyp /tmp/cares-reviewed/cares.gyp \ && cp /tmp/node-cares-integration/*.gn /tmp/node-cares-integration/*.gni /tmp/cares-reviewed/ \ && rm -rf /usr/src/node/deps/cares \ && mv /tmp/cares-reviewed /usr/src/node/deps/cares \ + && test -f /usr/src/node/deps/cares/config/linux/ares_config.h \ && grep -Fq "#define ARES_VERSION_STR \"${CARES_VERSION}\"" /usr/src/node/deps/cares/include/ares_version.h WORKDIR /usr/src/openssl