From 116eeec48cbd4ce028720e5a3d066bb9a7f5d57c Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 18:14:43 -0500 Subject: [PATCH] bus: row 199 mutants M06 and M26 now die; a test makes revoke's unlink fail and finds the token already gone from memory; the bus process refuses a boot config carrying publishTls (invalid-host-request) instead of dropping it, and the process test asserts the refusal (sage-1563, #1697) --- packages/bus/src/process.mjs | 6 ++++-- packages/bus/tests/credential-store.test.mjs | 18 ++++++++++++++++++ packages/bus/tests/publish.test.mjs | 13 +++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/bus/src/process.mjs b/packages/bus/src/process.mjs index e602f5164..727d869d7 100644 --- a/packages/bus/src/process.mjs +++ b/packages/bus/src/process.mjs @@ -110,8 +110,10 @@ if (!process.send) { booted = true; clearTimeout(timer); // `trackers` is S3's plain-data boot config; the adapter is loaded only when a business has one. - // Row 199: publishTls is the tests' scratch CA; no boot config sets it. - const { trackers, publishTls: _tls, ...config } = message.config ?? {}; + // Row 199: publishTls is the tests' scratch CA, for an in-process runtime only. The host's boot + // config never carries it, so a boot that does is refused rather than trusted. + const { trackers, ...config } = message.config ?? {}; + if (Object.hasOwn(config, 'publishTls')) throw new BusError('invalid-host-request'); if (trackers) { const { tasksAdapter } = await import('../../tasks/src/adapter.mjs'); config.tasks = tasksAdapter({ trackers }); diff --git a/packages/bus/tests/credential-store.test.mjs b/packages/bus/tests/credential-store.test.mjs index 3712abd02..a7417eff8 100644 --- a/packages/bus/tests/credential-store.test.mjs +++ b/packages/bus/tests/credential-store.test.mjs @@ -134,6 +134,24 @@ test('T5: a failed commit leaves the token on disk but not in memory, and a bad await assert.rejects(s.read(c), /credential-unavailable/); }); +test('T5: revoke drops the token from memory before the unlink, so a failed unlink still leaves it revoked', async (t) => { + const s = setup(t); + const c = s.open(); + c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); + assert.deepEqual(await s.read(c), { same: true, rotated: false }); + // An agent dir the bus can't write: the unlink fails with EACCES. + const agentDir = join(s.storeRoot, 'acme', 'coder'); + chmodSync(agentDir, 0o500); + try { + assert.throws(() => c.revoke('acme/coder', 'gitea'), /credential-file/); + } finally { + chmodSync(agentDir, 0o700); + } + assert.equal(existsSync(s.file), true); + assert.equal(s.state(c), 'revoked'); + await assert.rejects(s.read(c), /credential-unavailable/); +}); + test('T5: revoke of a {file} reference forgets the token and never unlinks the file', async (t) => { const s = setup(t); const path = join(s.root, 'outside-token'); diff --git a/packages/bus/tests/publish.test.mjs b/packages/bus/tests/publish.test.mjs index 28a4dcd0c..97fa517c2 100644 --- a/packages/bus/tests/publish.test.mjs +++ b/packages/bus/tests/publish.test.mjs @@ -247,7 +247,7 @@ test('runtime: withGitea keys from the open ticket only, and the store root is t // Row 199 (D9b, deviation 11): the bus process answers the socket's five publish ops from their own // launchOp lines. A LAUNCH_OPS name without one would get launch-op-refused. A boot config that -// carries publishTls still boots; process.mjs drops it before startBroker. +// carries publishTls is refused: the scratch CA is for an in-process runtime only. test('the bus process answers the publish ops from their own lines, not launch-op-refused', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-publish-ops-')); t.after(() => rmSync(root, { recursive: true, force: true })); @@ -260,7 +260,16 @@ test('the bus process answers the publish ops from their own lines, not launch-o child.send(m); return (await reply)[0]; }; - const config = { dataRoot: root, businesses: businesses(), launches: [], readers: [], publishTls: ['-c', 'http.sslCAInfo=/nonexistent'] }; + const config = { dataRoot: root, businesses: businesses(), launches: [], readers: [] }; + const tlsChild = fork(new URL('../src/process.mjs', import.meta.url), [], { stdio: ['ignore', 'ignore', 'ignore', 'ipc'] }); + t.after(() => { + if (tlsChild.exitCode === null) tlsChild.kill('SIGKILL'); + }); + const refused = once(tlsChild, 'message'); + const gone = once(tlsChild, 'exit'); + tlsChild.send({ op: 'boot', config: { ...config, publishTls: ['-c', 'http.sslCAInfo=/nonexistent'] } }); + assert.deepEqual((await refused)[0], { ok: false, error: 'invalid-host-request' }); + assert.equal((await gone)[0], 2); assert.equal((await ask({ op: 'boot', config })).ok, true); const unknown = 'not-a-ticket'; assert.deepEqual(await ask({ op: 'publishPush', ticket: unknown, id: 1 }), { ok: false, error: 'publish-unknown', id: 1 });