From 76775b0e949c928463ecf9ab4351564ca6b0ecb0 Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:44:18 +0200 Subject: [PATCH 1/2] maintenance: bind fixture reads and refresh artifact action pins --- .github/workflows/opencode-inference.yml | 2 +- .github/workflows/opencode-public-code.yml | 2 +- scripts/smoke_editor_ui.cjs | 21 +++- scripts/smoke_opencode_inference.cjs | 20 +++- tests/descriptor-reads.test.cjs | 110 +++++++++++++++++++++ tests/public-code-file.test.cjs | 18 +++- 6 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 tests/descriptor-reads.test.cjs diff --git a/.github/workflows/opencode-inference.yml b/.github/workflows/opencode-inference.yml index 6811759..cb45c6f 100644 --- a/.github/workflows/opencode-inference.yml +++ b/.github/workflows/opencode-inference.yml @@ -148,7 +148,7 @@ jobs: run: python3 -B scripts/opencode_ci.py export - name: Retain original closed evidence if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: opencode-qwen-${{ github.run_id }}-${{ github.sha }} path: build/ci-public-receipts/*.json diff --git a/.github/workflows/opencode-public-code.yml b/.github/workflows/opencode-public-code.yml index 0d827ea..6caf4b6 100644 --- a/.github/workflows/opencode-public-code.yml +++ b/.github/workflows/opencode-public-code.yml @@ -107,7 +107,7 @@ jobs: - name: Collect only the core's closed allowlisted receipts if: always() run: python3 -B scripts/public_code_ci.py export - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: name: public-code-proposal-${{ github.run_id }}-${{ github.sha }} diff --git a/scripts/smoke_editor_ui.cjs b/scripts/smoke_editor_ui.cjs index 505ead1..637baac 100644 --- a/scripts/smoke_editor_ui.cjs +++ b/scripts/smoke_editor_ui.cjs @@ -2,6 +2,7 @@ // Disposable guest only. Real CDP keyboard/mouse input, never a VS Code API shim. 'use strict'; const fs = require('node:fs/promises'); +const {constants} = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const {spawnSync} = require('node:child_process'); @@ -58,11 +59,23 @@ async function privateDirectory(value) { (info.mode & 0o7777) === 0o700, 'project_scope'); } +const checkFixture = value => demand(value, 'fixture_failed'); async function fixtureFile(project, name, max = 65536) { - const file = path.join(project, name), info = await fs.lstat(file); - demand(info.isFile() && !info.isSymbolicLink() && info.nlink === 1 && info.uid === process.getuid() && - info.size <= max && !(info.mode & 0o022), 'fixture_failed'); - return fs.readFile(file); + const file = path.join(project, name); + const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const info = await handle.stat(); + checkFixture(info.isFile() && info.nlink === 1 && info.uid === process.getuid() && info.size <= max && !(info.mode & 0o022)); + 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(); + checkFixture(size === info.size && ['size', 'mtimeMs', 'mode', 'uid', 'gid'].every(key => info[key] === after[key])); + return bytes.subarray(0, size); + } finally { await handle.close(); } } function journal(bytes) { diff --git a/scripts/smoke_opencode_inference.cjs b/scripts/smoke_opencode_inference.cjs index 2b3c2bb..f660458 100644 --- a/scripts/smoke_opencode_inference.cjs +++ b/scripts/smoke_opencode_inference.cjs @@ -3,6 +3,7 @@ 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs/promises'); +const {constants} = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const {createHash} = require('node:crypto'); @@ -81,9 +82,22 @@ async function owned(file, directory = false) { return info; } async function contents(project, name, maximum = 8192) { - const target = path.join(project, name), info = await owned(target); - assert.ok(info.size > 0 && info.size <= maximum); - return fs.readFile(target); + const target = path.join(project, name); + assert.equal(await fs.realpath(target), target); + const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + try { + const info = await handle.stat(); + assert.ok(info.isFile() && info.uid === process.getuid() && (info.mode & 0o6022) === 0 && info.size > 0 && info.size <= maximum); + 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(); + assert.ok(size === info.size && ['size', 'mtimeMs', 'mode', 'uid', 'gid'].every(key => info[key] === after[key])); + return bytes.subarray(0, size); + } finally { await handle.close(); } } function createTrialVerifier(project, createVerifier = createWorkspaceVerifier) { diff --git a/tests/descriptor-reads.test.cjs b/tests/descriptor-reads.test.cjs new file mode 100644 index 0000000..9dda372 --- /dev/null +++ b/tests/descriptor-reads.test.cjs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Isolated actual reader bodies with real files/descriptors, no guest/application execution. +'use strict'; +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": "scripts/smoke_editor_ui.cjs", + "name": "fixtureFile", + "sync": false, + "args": "project", + "rootOnly": false + }, + { + "source": "scripts/smoke_opencode_inference.cjs", + "name": "contents", + "sync": false, + "args": "project", + "rootOnly": false + }, + { + "source": "tests/public-code-file.test.cjs", + "name": "readFixtureBytes", + "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) { + 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: 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), 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), 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); 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); 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); + }); +} diff --git a/tests/public-code-file.test.cjs b/tests/public-code-file.test.cjs index acae5b3..b82d70c 100644 --- a/tests/public-code-file.test.cjs +++ b/tests/public-code-file.test.cjs @@ -9,6 +9,22 @@ const path = require('node:path'); const {createHash} = require('node:crypto'); const {capturePublicCodeFile, applyPublicCodeFile} = require('../src/public-code-file.cjs'); const {fixture: publicCore, INPUT, updateReport} = require('./public-code-fixture.cjs'); +function readFixtureBytes(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.ok(info.isFile() && info.uid === process.getuid() && 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.ok(size === info.size && ['size', 'mtimeMs', 'mode', 'uid', 'gid'].every(key => info[key] === after[key])); + return bytes.subarray(0, size); + } finally { fs.closeSync(fd); } +} function fixture(t) { const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-public-file-')); const file = path.join(workspace, 'selected.js'); @@ -66,7 +82,7 @@ test('approved exact raw replacement changes only selected file after validated const applied = await f.apply({approve: value => {edit = value; return true;}}); assert.equal(applied.applied, true); assert.equal(fs.readFileSync(f.file, 'utf8'), 'export const value = 2;\n'); - assert.equal(fs.readFileSync(tests, 'utf8'), 'original independent test bytes'); + assert.equal(readFixtureBytes(tests).toString('utf8'), 'original independent test bytes'); assert.notEqual(fs.statSync(f.file).ino, oldIdentity); // No write through old inode. assert.equal(fs.statSync(f.file).mode & 0o777, 0o600); assert.equal(edit.replacement, 'export const value = 2;\n'); From 68375f3d2c6fd208a42bf5b22b5503672a97b33f Mon Sep 17 00:00:00 2001 From: Erik Le Blansch <58982453+erikleblansch@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:55:30 +0200 Subject: [PATCH 2/2] test: retain exact reviewed smoke-driver hash --- tests/test_opencode_native_cpu.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_opencode_native_cpu.py b/tests/test_opencode_native_cpu.py index ad35351..04cae51 100644 --- a/tests/test_opencode_native_cpu.py +++ b/tests/test_opencode_native_cpu.py @@ -249,11 +249,12 @@ def test_guest_keeps_original_task_and_inference_budgets_and_joins_native_unit(s "if report['units_empty'] and report['provision_group_joined']:"): self.assertIn(text, source) task_driver = (ROOT / 'scripts/smoke_opencode_inference.cjs').read_bytes() - # Exact source with opt-in output selection, usable in a shallow checkout. + # Exact source with opt-in output selection and descriptor-bound fixture reads. + # The reviewed delta changes only contents() and its fs.constants import. # Focused JS tests retain the original prompt, approvals and round bounds; # no regenerated answers or claim of model quality. self.assertEqual(hashlib.sha256(task_driver).hexdigest(), - '92ca6da4a1cff07f07fd9e56afe28f853c2868f7d111c6cdc5350145bb54cba0') + '0f395f215d11b3ebc868299a7695908511724778b72d2966b9669ccaf20ccf13') def test_native_memory_requires_actual_zero_swap_and_oom_not_just_limits(self): with tempfile.TemporaryDirectory() as temp, patch.object(TRIAL, 'cgroup', return_value=Path(temp)):