From 52b17e1cd13e777c63cf0d3481a733d98fa10e7d Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:41:10 +0200 Subject: [PATCH 1/4] maintenance: bind fixture reads and refresh artifact action pins --- .github/workflows/alpha-topology.yml | 10 +- .github/workflows/dns-cache-proof.yml | 2 +- .../workflows/helper-boundary-evidence.yml | 6 +- .github/workflows/native-cpu-contract.yml | 2 +- .../workflows/native-session-diagnostics.yml | 2 +- .../workflows/protected-compute-admission.yml | 6 +- .../workflows/protected-compute-verifier.yml | 2 +- .github/workflows/quality.yml | 4 +- .github/workflows/storage-lock-lifecycle.yml | 2 +- .../workflows/weather-android-delivery.yml | 2 +- .github/workflows/weather-decoder-abi.yml | 2 +- .../workflows/weather-overmorrow-delivery.yml | 2 +- .../workflows/weather-products-delivery.yml | 2 +- scripts/test-diagnose-backport-lock.py | 2 +- tests/integration/agent-private-task-code.cjs | 20 +++- tests/integration/cloud-private-file-sdk.mjs | 22 +++- tests/integration/test-descriptor-reads.mjs | 106 ++++++++++++++++++ 17 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 tests/integration/test-descriptor-reads.mjs diff --git a/.github/workflows/alpha-topology.yml b/.github/workflows/alpha-topology.yml index 46a30e1c0..7f9c79bc1 100644 --- a/.github/workflows/alpha-topology.yml +++ b/.github/workflows/alpha-topology.yml @@ -2131,7 +2131,7 @@ jobs: - name: Upload closed transaction TEST evidence if: always() && env.VOLPAROSSA_ALPHA_SCENARIO == 'transaction-abci' && env.VOLPAROSSA_ALPHA_OUTPUT != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: alpha-production-helper-topology-${{ github.run_id }} path: | @@ -2153,7 +2153,7 @@ jobs: - name: Upload closed protected quote refusal evidence if: always() && env.VOLPAROSSA_ALPHA_SCENARIO == 'protected-quote' && env.VOLPAROSSA_ALPHA_OUTPUT != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: alpha-production-helper-topology-${{ github.run_id }} path: | @@ -2164,7 +2164,7 @@ jobs: - name: Upload bounded topology diagnostics if: always() && env.VOLPAROSSA_ALPHA_SCENARIO != 'agent-private-conversation' && env.VOLPAROSSA_ALPHA_SCENARIO != 'agent-native-coding' && env.VOLPAROSSA_ALPHA_SCENARIO != 'transaction-abci' && env.VOLPAROSSA_ALPHA_SCENARIO != 'protected-quote' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: alpha-production-helper-topology-${{ github.run_id }} path: ${{ env.VOLPAROSSA_ALPHA_OUTPUT }} @@ -2173,7 +2173,7 @@ jobs: - name: Upload bounded private conversation fixture evidence if: always() && env.VOLPAROSSA_ALPHA_SCENARIO == 'agent-private-conversation' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: alpha-production-helper-topology-${{ github.run_id }} path: | @@ -2202,7 +2202,7 @@ jobs: - name: Upload bounded native coding fixture evidence if: always() && env.VOLPAROSSA_ALPHA_SCENARIO == 'agent-native-coding' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: alpha-production-helper-topology-${{ github.run_id }} path: | diff --git a/.github/workflows/dns-cache-proof.yml b/.github/workflows/dns-cache-proof.yml index f4cab307c..8b8fd9ac6 100644 --- a/.github/workflows/dns-cache-proof.yml +++ b/.github/workflows/dns-cache-proof.yml @@ -128,7 +128,7 @@ jobs: - name: Upload exact-source public wire and bounded collector evidence if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: dns-cache-builtin-root-${{ github.run_id }} path: ${{ env.VOLPAROSSA_DNS_PROOF_OUTPUT }} diff --git a/.github/workflows/helper-boundary-evidence.yml b/.github/workflows/helper-boundary-evidence.yml index 2d088e658..2082e762a 100644 --- a/.github/workflows/helper-boundary-evidence.yml +++ b/.github/workflows/helper-boundary-evidence.yml @@ -658,7 +658,7 @@ jobs: steps.vm_proof.outputs.exit_code == '0' && steps.host_validation.outcome == 'success' && steps.verify_kvm_state.outcome == 'success' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: helper-boundary-evidence-${{ github.sha }} path: | @@ -679,7 +679,7 @@ jobs: steps.host_validation.outcome != 'success' || steps.verify_kvm_state.outcome != 'success') && steps.retained_artifacts.outputs.diagnostics_any == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: helper-boundary-diagnostics-${{ github.sha }} path: | @@ -697,7 +697,7 @@ jobs: steps.vm_proof.outputs.exit_code == '0' && steps.host_validation.outcome == 'success' && steps.verify_kvm_state.outcome == 'success' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: helper-restart-exact-present-evidence-${{ github.sha }} path: | diff --git a/.github/workflows/native-cpu-contract.yml b/.github/workflows/native-cpu-contract.yml index 3e65f8344..0a4391ce1 100644 --- a/.github/workflows/native-cpu-contract.yml +++ b/.github/workflows/native-cpu-contract.yml @@ -166,7 +166,7 @@ jobs: ) 2>&1 | tee build/native-contract/source-check.log - name: Retain original source-only reports and logs, including failures if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: native-source-${{ matrix.mode }}-${{ github.sha }} path: | diff --git a/.github/workflows/native-session-diagnostics.yml b/.github/workflows/native-session-diagnostics.yml index 59cfbf849..d73be7f09 100644 --- a/.github/workflows/native-session-diagnostics.yml +++ b/.github/workflows/native-session-diagnostics.yml @@ -261,7 +261,7 @@ jobs: - name: Preserve only source, inventory and original check evidence including failures if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: native-session-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} retention-days: 14 diff --git a/.github/workflows/protected-compute-admission.yml b/.github/workflows/protected-compute-admission.yml index a9dd18e92..3fc236eeb 100644 --- a/.github/workflows/protected-compute-admission.yml +++ b/.github/workflows/protected-compute-admission.yml @@ -100,7 +100,7 @@ jobs: run: python3 -B tests/integration/protected-compute-quote.py receipt - name: Retain only bounded public source lock and test evidence if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: protected-compute-core-evidence-${{ github.sha }} retention-days: 14 @@ -123,7 +123,7 @@ jobs: ${{ runner.temp }}/volparossa-protected-compute-admission/evidence/clippy.log - name: Retain bounded guest software evidence if: always() && (github.ref == 'refs/heads/feature/protected-compute-guest' || github.ref == 'refs/heads/feature/protected-compute-quote-exchange') - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: protected-compute-guest-software-${{ github.sha }} retention-days: 14 @@ -134,7 +134,7 @@ jobs: ${{ runner.temp }}/volparossa-protected-compute-admission/guest-evidence/report.json - name: Retain bounded quote software evidence if: always() && github.ref == 'refs/heads/feature/protected-compute-quote-exchange' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: protected-compute-quote-software-${{ github.sha }} retention-days: 14 diff --git a/.github/workflows/protected-compute-verifier.yml b/.github/workflows/protected-compute-verifier.yml index df826bc48..a8bac4d58 100644 --- a/.github/workflows/protected-compute-verifier.yml +++ b/.github/workflows/protected-compute-verifier.yml @@ -97,7 +97,7 @@ jobs: "$VERIFIER_SOURCE" build/protected-compute-verifier/evidence "${statuses[0]}" "${statuses[1]}" - name: Retain original public fixture evidence only if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: protected-compute-public-fixtures-${{ github.sha }} retention-days: 14 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 13229d614..42de716e7 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -82,7 +82,7 @@ jobs: sh -n tests/integration/content-provider-pressure-smoke.sh node --version node -e 'if (Number(process.versions.node.split(".")[0]) < 22) process.exit(1)' - node --test tests/integration/test-weather-source-observer.mjs + node --test tests/integration/test-weather-source-observer.mjs tests/integration/test-descriptor-reads.mjs python3 -B tests/integration/test-weather-products-delivery.py python3 -B tests/integration/test-weather-products-hosted-wiring.py python3 -B tests/integration/test-weather-overmorrow-hosted-wiring.py @@ -182,7 +182,7 @@ jobs: - name: Preserve failed standalone lock reconciliation if: ${{ failure() && steps.fetch-dependencies.outputs.backport_lock_diagnostic == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: backport-lock-diagnostic-${{ github.run_id }} path: | diff --git a/.github/workflows/storage-lock-lifecycle.yml b/.github/workflows/storage-lock-lifecycle.yml index 0497400d1..decdeb416 100644 --- a/.github/workflows/storage-lock-lifecycle.yml +++ b/.github/workflows/storage-lock-lifecycle.yml @@ -182,7 +182,7 @@ jobs: - name: Retain only regression observations and logs if: always() && steps.scope.outcome == 'success' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: storage-lock-lifecycle-${{ github.run_id }}-${{ github.run_attempt }} path: | diff --git a/.github/workflows/weather-android-delivery.yml b/.github/workflows/weather-android-delivery.yml index 6adf81266..960d735ac 100644 --- a/.github/workflows/weather-android-delivery.yml +++ b/.github/workflows/weather-android-delivery.yml @@ -372,7 +372,7 @@ jobs: --manifest "$WEATHER_MANIFEST" --kvm-cleanup "$WEATHER_INPUTS/kvm-cleanup.json" - name: Upload only closed evidence and pinned-input provenance if: always() && env.WEATHER_OUTPUT != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: weather-android-delivery-${{ github.sha }} retention-days: 14 diff --git a/.github/workflows/weather-decoder-abi.yml b/.github/workflows/weather-decoder-abi.yml index be76928c9..bc62df0d3 100644 --- a/.github/workflows/weather-decoder-abi.yml +++ b/.github/workflows/weather-decoder-abi.yml @@ -115,7 +115,7 @@ jobs: --expected-commit "$GITHUB_SHA" - name: Retain bounded ABI results only if: always() && env.VOLPAROSSA_ABI_OUTPUT != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: weather-decoder-debian13-${{ github.sha }} retention-days: 14 diff --git a/.github/workflows/weather-overmorrow-delivery.yml b/.github/workflows/weather-overmorrow-delivery.yml index f6f370b2d..02530dc3a 100644 --- a/.github/workflows/weather-overmorrow-delivery.yml +++ b/.github/workflows/weather-overmorrow-delivery.yml @@ -302,7 +302,7 @@ jobs: PY - name: Retain bounded original evidence, never the input bundle or profiles if: always() && env.WEATHER_OUTPUT != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: weather-overmorrow-delivery-${{ github.sha }} retention-days: 14 diff --git a/.github/workflows/weather-products-delivery.yml b/.github/workflows/weather-products-delivery.yml index bab5a8233..9ff714bac 100644 --- a/.github/workflows/weather-products-delivery.yml +++ b/.github/workflows/weather-products-delivery.yml @@ -269,7 +269,7 @@ jobs: PY - name: Retain bounded original evidence, never the input bundle or profiles if: always() && env.WEATHER_OUTPUT != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: weather-products-delivery-${{ github.sha }} retention-days: 14 diff --git a/scripts/test-diagnose-backport-lock.py b/scripts/test-diagnose-backport-lock.py index 817a10c38..82cd152da 100644 --- a/scripts/test-diagnose-backport-lock.py +++ b/scripts/test-diagnose-backport-lock.py @@ -228,7 +228,7 @@ def test_harness_failure_preserved_after_diagnostic_success_or_failure(self): def test_failure_only_exact_artifact_allowlist(self): step = workflow_step("Preserve failed standalone lock reconciliation") self.assertIn(" if: ${{ failure() && steps.fetch-dependencies.outputs.backport_lock_diagnostic == 'true' }}", step) - self.assertIn(" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1", step) + self.assertIn(" uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2", step) start = step.index(" path: |") + 1 paths = [] for line in step[start:]: diff --git a/tests/integration/agent-private-task-code.cjs b/tests/integration/agent-private-task-code.cjs index 4b22dc068..031a76fb3 100644 --- a/tests/integration/agent-private-task-code.cjs +++ b/tests/integration/agent-private-task-code.cjs @@ -14,6 +14,22 @@ function save(name, value) { assert(Buffer.byteLength(data) <= 65536); fs.writeFileSync(path.join(root, name), data, { flag: 'wx', mode: 0o600 }); } +function readPrivateInput(file) { + const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); + try { + const info = fs.fstatSync(fd); + assert(info.isFile() && info.uid === process.getuid() && (info.mode & 0o7777) === 0o600 && info.size <= 65536); + const bytes = Buffer.alloc(info.size + 1); let size = 0; + while (size < bytes.length) { + const count = fs.readSync(fd, bytes, size, bytes.length - size, size); + if (count === 0) break; + size += count; + } + const after = fs.fstatSync(fd); + assert(size === info.size && ['size', 'mtimeMs', 'mode', 'uid', 'gid'].every(key => info[key] === after[key])); + return bytes.subarray(0, size); + } finally { fs.closeSync(fd); } +} async function main() { assert.equal(root, '/home/vpci/private-code'); assert.equal(process.getuid(), process.geteuid()); @@ -22,9 +38,7 @@ async function main() { assert.notEqual(fs.readlinkSync('/proc/self/ns/net'), parentNetwork); assert(Object.keys(os.networkInterfaces()).every(name => name === 'lo')); const { PrivateCompute } = require(path.join(root, 'src/private-compute.cjs')); - const info = fs.lstatSync(inputPath); - assert(info.isFile() && info.uid === process.getuid() && (info.mode & 0o7777) === 0o600 && info.size <= 65536); - const input = JSON.parse(fs.readFileSync(inputPath, 'utf8')); + const input = JSON.parse(readPrivateInput(inputPath)); assert.equal(input.version, 1); assert.equal(input.visibility, 'private_local'); assert.equal(input.question, 'What string does testIdentifier return? Answer with only the string.'); diff --git a/tests/integration/cloud-private-file-sdk.mjs b/tests/integration/cloud-private-file-sdk.mjs index 06fa4c106..7ad0b175c 100644 --- a/tests/integration/cloud-private-file-sdk.mjs +++ b/tests/integration/cloud-private-file-sdk.mjs @@ -3,7 +3,8 @@ import assert from 'node:assert/strict'; import { createHash, randomBytes } from 'node:crypto'; import { spawn } from 'node:child_process'; -import { lstat, readFile, realpath, mkdir, readdir, writeFile } from 'node:fs/promises'; +import { constants } from 'node:fs'; +import { open, lstat, readFile, realpath, mkdir, readdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { setTimeout as delay } from 'node:timers/promises'; @@ -52,10 +53,21 @@ async function privateJSON(file, value) { await writeFile(file, JSON.stringify(value) + '\n', { flag: 'wx', mode: 0o600 }); } async function publicBytes(file) { - const info = await lstat(file); - insist(info.isFile() && info.uid === 0 && !(info.mode & 0o222) - && info.size <= 2 * 1024 ** 2 && await realpath(file) === file); - return readFile(file); + insist(await realpath(file) === file); + const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const info = await handle.stat(); + insist(info.isFile() && info.uid === 0 && !(info.mode & 0o222) && info.size <= 2 * 1024 ** 2); + const bytes = Buffer.alloc(info.size + 1); let size = 0; + while (size < bytes.length) { + const { bytesRead } = await handle.read(bytes, size, bytes.length - size, size); + if (bytesRead === 0) break; + size += bytesRead; + } + const after = await handle.stat(); + insist(size === info.size && ['size', 'mtimeMs', 'mode', 'uid', 'gid'].every(key => info[key] === after[key])); + return bytes.subarray(0, size); + } finally { await handle.close(); } } async function sdkProvenance() { const bytes = await publicBytes(`${SDK}/receipt.json`); diff --git a/tests/integration/test-descriptor-reads.mjs b/tests/integration/test-descriptor-reads.mjs new file mode 100644 index 000000000..e2d5b0d1c --- /dev/null +++ b/tests/integration/test-descriptor-reads.mjs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Isolated actual reader bodies with real files/descriptors, no guest/application execution. +import {createRequire} from 'node:module'; +import {fileURLToPath} from 'node:url'; +const require = createRequire(import.meta.url); +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const {test} = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const fsp = require('node:fs/promises'); +const path = require('node:path'); +const os = require('node:os'); +const vm = require('node:vm'); +const ROOT = path.resolve(__dirname, '../..'); +const readers = [ + { + "source": "tests/integration/cloud-private-file-sdk.mjs", + "name": "publicBytes", + "sync": false, + "args": "file", + "rootOnly": true + }, + { + "source": "tests/integration/agent-private-task-code.cjs", + "name": "readPrivateInput", + "sync": true, + "args": "file", + "rootOnly": false + } +]; +function reader(config, afterStat = () => {}, beforeRead = () => {}) { + const source = fs.readFileSync(path.join(ROOT, config.source), 'utf8'); + const start = source.indexOf((config.sync ? 'function ' : 'async function ') + config.name + '('); + assert.ok(start >= 0); + const end = source.indexOf('\n}', start) + 2; + assert.ok(end > start); + const handles = [], closed = [], capacities = []; + const wrappedFS = {...fs, ...(!config.sync ? fsp : {})}; + if (config.sync) { + wrappedFS.openSync = (...args) => { const fd = fs.openSync(...args); handles.push(fd); return fd; }; + wrappedFS.fstatSync = fd => { const info = fs.fstatSync(fd); afterStat(); return info; }; + wrappedFS.readSync = (fd, bytes, ...args) => { + capacities.push(bytes.length); beforeRead(); return fs.readSync(fd, bytes, ...args); + }; + wrappedFS.closeSync = fd => { closed.push(fd); return fs.closeSync(fd); }; + } + async function open(...args) { + const handle = await fsp.open(...args); handles.push(handle.fd); + return {stat: async () => {const info = await handle.stat(); afterStat(); return info;}, + read: async (bytes, ...rest) => {capacities.push(bytes.length); beforeRead(); return handle.read(bytes, ...rest);}, + close: async () => {closed.push(handle.fd); await handle.close();}}; + } + wrappedFS.open = open; + const demand = value => assert.ok(value); + const fn = vm.runInNewContext('(' + source.slice(start, end) + ')', {fs: wrappedFS, open, + constants: fs.constants, realpath: fsp.realpath, realpathSync: fs.realpathSync, + path, Buffer, process, assert, insist: demand, require: demand, demand, checkFixture: demand, + MAX_BYTES: 2 * 1024 * 1024}); + const call = file => config.args === 'expected' ? fn(file, {bytes: 12}) + : config.args === 'project' ? fn(path.dirname(file), path.basename(file)) : fn(file); + return {call, handles, closed, capacities}; +} +function fixture(t, rootOnly = false) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-descriptor-')); + fs.chmodSync(directory, 0o700); + const file = path.join(directory, 'fixture'); + fs.writeFileSync(file, 'trusted-data', {mode: rootOnly && process.getuid() === 0 ? 0o444 : 0o600}); + t.after(() => fs.rmSync(directory, {recursive: true, force: false})); + return {directory, file}; +} +for (const config of readers) { + test(config.name + ': leaf symlink is rejected without reading its target', async t => { + const f = fixture(t, config.rootOnly), linked = path.join(f.directory, 'link'); + fs.symlinkSync(f.file, linked); + const r = reader(config); + await assert.rejects(async () => r.call(linked)); + assert.equal(r.capacities.length, 0); assert.deepEqual(r.closed, r.handles); + }); + if (config.rootOnly && process.getuid() !== 0) { + test(config.name + ': unprivileged ownership fails closed and closes descriptor', async t => { + const f = fixture(t, config.rootOnly), r = reader(config); + await assert.rejects(async () => r.call(f.file)); + assert.equal(r.capacities.length, 0); assert.deepEqual(r.closed, r.handles); + }); + continue; // No simulated root authority in an unprivileged local test. + } + test(config.name + ': pathname replacement after stat reads the checked descriptor', async t => { + const f = fixture(t, config.rootOnly); let replaced = false; + const r = reader(config, () => { + if (replaced) return; replaced = true; + fs.renameSync(f.file, path.join(f.directory, 'original')); + fs.writeFileSync(f.file, 'untrusted!!!', {mode: 0o600}); + }); + assert.equal((await r.call(f.file)).toString(), 'trusted-data'); + assert.equal(replaced, true); assert.deepEqual(r.closed, r.handles); + }); + test(config.name + ': concurrent growth is bounded and closes on rejection', async t => { + const f = fixture(t, config.rootOnly); let changed = false; + const r = reader(config, () => {}, () => { + if (changed) return; changed = true; fs.appendFileSync(f.file, Buffer.alloc(1024 * 1024)); + }); + await assert.rejects(async () => r.call(f.file)); + assert.ok(r.capacities.length > 0); assert.ok(r.capacities.every(size => size === 13)); + assert.deepEqual(r.closed, r.handles); + }); +} From 18a420d60c7ec2e5030565a378a8233b6e3f4868 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:55:28 +0200 Subject: [PATCH 2/4] ci: pin compatible Ubuntu Protobuf compiler and libraries --- .github/workflows/native-session-diagnostics.yml | 5 ++++- .github/workflows/quality.yml | 7 +++++-- .github/workflows/storage-lock-lifecycle.yml | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/native-session-diagnostics.yml b/.github/workflows/native-session-diagnostics.yml index d73be7f09..ba8ebaa87 100644 --- a/.github/workflows/native-session-diagnostics.yml +++ b/.github/workflows/native-session-diagnostics.yml @@ -118,9 +118,12 @@ jobs: test "$GITHUB_ACTIONS" = true test "$RUNNER_ENVIRONMENT" = github-hosted test "$(dpkg --print-architecture)" = amd64 + # Same Noble security compiler/library family as Quality (2026-10-20). sudo -n apt-get update sudo -n apt-get install --yes --no-install-recommends \ - protobuf-compiler=3.21.12-8.2ubuntu0.3 + protobuf-compiler=3.21.12-8.2ubuntu0.4 \ + libprotoc32t64=3.21.12-8.2ubuntu0.4 \ + libprotobuf32t64=3.21.12-8.2ubuntu0.4 test "$(protoc --version)" = 'libprotoc 3.21.12' - name: Check the closed Python collector contracts diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 42de716e7..484a62e8a 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -58,10 +58,13 @@ jobs: test "$RUNNER_ENVIRONMENT" = github-hosted test "$(dpkg --print-architecture)" = amd64 # Official Noble security package; signed apt metadata authenticates it. - # https://packages.ubuntu.com/noble/amd64/protobuf-compiler + # Compiler and library family from Noble security, published 2026-10-20. + # https://security.ubuntu.com/ubuntu/dists/noble-security/universe/binary-amd64/Packages.xz sudo apt-get update sudo apt-get install --yes --no-install-recommends \ - protobuf-compiler=3.21.12-8.2ubuntu0.3 + protobuf-compiler=3.21.12-8.2ubuntu0.4 \ + libprotoc32t64=3.21.12-8.2ubuntu0.4 \ + libprotobuf32t64=3.21.12-8.2ubuntu0.4 test "$(protoc --version)" = 'libprotoc 3.21.12' - name: Verify original ABCI schemas and inert fixture contracts diff --git a/.github/workflows/storage-lock-lifecycle.yml b/.github/workflows/storage-lock-lifecycle.yml index decdeb416..be7488c93 100644 --- a/.github/workflows/storage-lock-lifecycle.yml +++ b/.github/workflows/storage-lock-lifecycle.yml @@ -79,9 +79,12 @@ jobs: test "$GITHUB_ACTIONS" = true test "$RUNNER_ENVIRONMENT" = github-hosted test "$(dpkg --print-architecture)" = amd64 + # Same Noble security compiler/library family as Quality (2026-10-20). sudo -n apt-get update sudo -n apt-get install --yes --no-install-recommends \ - protobuf-compiler=3.21.12-8.2ubuntu0.3 + protobuf-compiler=3.21.12-8.2ubuntu0.4 \ + libprotoc32t64=3.21.12-8.2ubuntu0.4 \ + libprotobuf32t64=3.21.12-8.2ubuntu0.4 test "$(protoc --version)" = 'libprotoc 3.21.12' - name: Check out unchanged locking plus only the three regressions From 695a1dad627ab92aa827c2d760a888884bc4a210 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:04:09 +0200 Subject: [PATCH 3/4] fix(deps): update HPKE and zeroize across both lock contexts --- Cargo.lock | 10 ++++++---- Cargo.toml | 2 +- THIRD_PARTY_LICENSES.md | 2 +- fuzz/Cargo.lock | 10 ++++++---- tests/integration/protected-compute-admission.py | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9167b4b9d..835304113 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -864,6 +864,7 @@ dependencies = [ "fiat-crypto 0.3.0", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -1710,9 +1711,9 @@ dependencies = [ [[package]] name = "hpke" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5130e119706b4d8c2180da6126f7e60b6c38c2d340d539219f57051f0a7af7" +checksum = "a5324110b02044183df000f0bd7d2d7e61f1000631508627e77f63983b6fdffb" dependencies = [ "aead 0.6.1", "chacha20poly1305 0.11.0", @@ -5825,6 +5826,7 @@ checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ "curve25519-dalek 5.0.0", "rand_core 0.10.1", + "zeroize", ] [[package]] @@ -6018,9 +6020,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" dependencies = [ "serde", "zeroize_derive", diff --git a/Cargo.toml b/Cargo.toml index de739d377..39ae9ce92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ futures = "0.3.31" getrandom = "0.4.3" hex = { version = "0.4.3", features = ["serde"] } hkdf = "0.12.4" -hpke = { version = "=0.14.0", default-features = false, features = ["alloc", "chacha", "x25519"] } +hpke = { version = "=0.14.1", default-features = false, features = ["alloc", "chacha", "x25519"] } h3 = "=0.0.8" h3-quinn = "=0.0.10" hickory-proto = "0.25.2" diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 313cc5f5a..f415b05e8 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -920,7 +920,7 @@ The package does not depend on or start the standalone Unbound daemon. | serde, serde_json, serde_yaml, prost | configuration and wire encoding | MIT or Apache-2.0/MIT | | rust-libp2p and Quinn | decentralised control plane and QUIC | MIT | | rustls, ring, ed25519-dalek, x25519-dalek, Argon2 | transport, identity, and key protection | mixed permissive licenses; inspect every resolved crate | -| `hpke` 0.14.0 (`rust-hpke`) | RFC 9180 route-credential sealing, exact crates.io pin in `Cargo.lock` | MIT/Apache-2.0; upstream `https://github.com/rozbb/rust-hpke` | +| `hpke` 0.14.1 (`rust-hpke`) | RFC 9180 route-credential sealing, exact crates.io pin in `Cargo.lock` | MIT/Apache-2.0; upstream `https://github.com/rozbb/rust-hpke` | | rusqlite and SQLite bundle | bounded local peer/session state | MIT for wrapper; SQLite is public domain | | rtnetlink, netlink-sys, wireguard-uapi, nix, libc | Linux networking and OS boundary | mixed permissive licenses; inspect every resolved crate | diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index de38045ed..368aa5b18 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -560,6 +560,7 @@ dependencies = [ "fiat-crypto 0.3.0", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -1159,9 +1160,9 @@ dependencies = [ [[package]] name = "hpke" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5130e119706b4d8c2180da6126f7e60b6c38c2d340d539219f57051f0a7af7" +checksum = "a5324110b02044183df000f0bd7d2d7e61f1000631508627e77f63983b6fdffb" dependencies = [ "aead 0.6.1", "chacha20poly1305 0.11.0", @@ -3820,6 +3821,7 @@ checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ "curve25519-dalek 5.0.0", "rand_core 0.10.1", + "zeroize", ] [[package]] @@ -3945,9 +3947,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" dependencies = [ "serde", "zeroize_derive", diff --git a/tests/integration/protected-compute-admission.py b/tests/integration/protected-compute-admission.py index 16c0476f0..355518bd1 100644 --- a/tests/integration/protected-compute-admission.py +++ b/tests/integration/protected-compute-admission.py @@ -41,7 +41,7 @@ '9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292'), ('num-traits', '0.2.19', REGISTRY): (('libm', '0.2.16', REGISTRY), '071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841'), - ('generic-array', '0.14.7', REGISTRY): (('zeroize', '1.9.0', REGISTRY), + ('generic-array', '0.14.7', REGISTRY): (('zeroize', '1.9.1', REGISTRY), '85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a'), ('lazy_static', '1.5.0', REGISTRY): (('spin', '0.9.9', REGISTRY), 'bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe'), From fe9d6623ceec64dd1311577310f91ee2200137ed Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:20:45 +0200 Subject: [PATCH 4/4] test: align helper artifact contract with reviewed action pin --- tests/helper/test-helper-boundary-vm-contract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helper/test-helper-boundary-vm-contract.sh b/tests/helper/test-helper-boundary-vm-contract.sh index 580287b6d..6f456e7cf 100755 --- a/tests/helper/test-helper-boundary-vm-contract.sh +++ b/tests/helper/test-helper-boundary-vm-contract.sh @@ -2263,7 +2263,7 @@ for exact_workflow_text in \ '"$proof_mode_flag"' \ '--expected-source-ref "$SELECTED_REF"' \ 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' \ - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' \ + 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' \ 'persist-credentials: false' \ 'ref: ${{ github.ref }}' \ 'test "$(git symbolic-ref --quiet --short HEAD)" = main' \