返回 CodeWhale
windows-sandbox-probe.test.mjs
根目录 / crates / tui / extension-host / test / windows-sandbox-probe.test.mjs
1 // Fixed diagnostic logic, not a Windows/kernel isolation receipt.
2 import { test } from 'node:test'
3 import assert from 'node:assert/strict'
4 import fs from 'node:fs'
5 import net from 'node:net'
6 import cp from 'node:child_process'
7 import { EventEmitter } from 'node:events'
8 import { tmpdir } from 'node:os'
9 import { join } from 'node:path'
10 import { windowsSandboxProbe } from '../src/windows-sandbox-probe.mjs'
11
12 const receipt = { version: 1, data_roundtrip: true, outside_read_denied: true, outside_write_denied: true, network_denied: true, descendant_denied: true }
13 const env = {
14 CODEWHALE_WINDOWS_PROBE_INSIDE: '/inside/marker', CODEWHALE_WINDOWS_PROBE_OUTSIDE: '/outside/write',
15 CODEWHALE_WINDOWS_PROBE_READS: JSON.stringify(['/codex/auth', '/dsh/credentials', '/builtin/state']),
16 CODEWHALE_WINDOWS_PROBE_PORT: '12345', CODEWHALE_WINDOWS_PROBE_CHILD_ARGS: JSON.stringify(['fixed-probe']),
17 }
18 function controls(t, options = {}) {
19 let value
20 const outputPath = `${env.CODEWHALE_WINDOWS_PROBE_INSIDE}.receipt`
21 const state = { created: false, closed: false, removed: false, killed: 0, events: [], positions: [], output: Buffer.from(options.output ?? JSON.stringify(receipt)) }
22 const originals = [fs.writeFileSync, fs.readFileSync, fs.unlinkSync, fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync, net.connect, cp.spawn]
23 t.after(() => { [fs.writeFileSync, fs.readFileSync, fs.unlinkSync, fs.openSync, fs.fstatSync, fs.readSync, fs.closeSync, net.connect, cp.spawn] = originals })
24 const denied = (code) => Object.assign(new Error(code), { code })
25 fs.writeFileSync = (path, content) => {
26 if (path === env.CODEWHALE_WINDOWS_PROBE_INSIDE) { value = content; return }
27 if (options.outsideWrite) return
28 throw denied('EACCES')
29 }
30 fs.readFileSync = (path) => {
31 if (path === env.CODEWHALE_WINDOWS_PROBE_INSIDE) return value
32 throw denied(options.readError ?? 'EACCES')
33 }
34 fs.unlinkSync = (path) => {
35 if (path !== outputPath) { value = undefined; return }
36 assert.equal(state.closed, true, 'close the owned descriptor before removing its file')
37 state.removed = true; state.events.push('unlink')
38 }
39 fs.openSync = (path, flags) => {
40 assert.equal(path, outputPath)
41 assert.equal(flags, 'wx+')
42 if (options.openError) throw denied(options.openError)
43 state.created = true
44 return 71
45 }
46 fs.fstatSync = (fd) => {
47 assert.equal(fd, 71); assert.equal(state.closed, false)
48 return { size: options.statSize ?? state.output.length }
49 }
50 fs.readSync = (fd, buffer, offset, length, position) => {
51 assert.equal(fd, 71); assert.equal(state.closed, false)
52 assert.ok(length <= 4097)
53 state.positions.push(position)
54 return state.output.copy(buffer, offset, position, Math.min(position + length, position + (options.shortRead ?? length)))
55 }
56 fs.closeSync = (fd) => {
57 assert.equal(fd, 71); assert.equal(state.created, true); assert.equal(state.closed, false)
58 state.closed = true; state.events.push('file-close')
59 }
60 net.connect = () => {
61 const socket = new EventEmitter()
62 socket.destroy = () => {}
63 if (!options.timeout) process.nextTick(() => {
64 if (options.networkConnect) socket.emit('connect')
65 else socket.emit('error', denied(options.networkError ?? 'EACCES'))
66 })
67 return socket
68 }
69 cp.spawn = (program, args, launch) => {
70 assert.equal(program, process.execPath)
71 assert.deepEqual(args, ['fixed-probe'])
72 assert.equal(launch.env.CODEWHALE_WINDOWS_PROBE_DESCENDANT, '1')
73 assert.equal(launch.env.CODEWHALE_WINDOWS_PROBE_INSIDE, `${env.CODEWHALE_WINDOWS_PROBE_INSIDE}.child`)
74 assert.deepEqual(launch.stdio, [0, 71, 71], 'no NUL open or runtime-created pipes')
75 if (options.spawnThrow) throw denied('EPERM')
76 const child = new EventEmitter()
77 state.child = child
78 const close = (code, signal) => { state.events.push('child-close'); child.emit('close', code, signal) }
79 child.kill = () => {
80 state.killed += 1; state.events.push('kill')
81 if (!options.hangAfterKill) process.nextTick(() => close(null, 'SIGTERM'))
82 return true
83 }
84 if (options.spawnError) process.nextTick(() => child.emit('error', denied('EPERM')))
85 else if (!options.hang) process.nextTick(() => close(options.exitCode ?? 0, options.signal ?? null))
86 return child
87 }
88 return state
89 }
90
91 test('fixed probe requires exact own-data and parent/descendant denial receipt', async (t) => {
92 const state = controls(t, { shortRead: 7 })
93 assert.deepEqual(await windowsSandboxProbe(env), receipt)
94 assert.equal(state.positions[0], 0)
95 assert.ok(state.positions.length > 2, 'short reads must be completed with explicit positions')
96 assert.deepEqual(state.events, ['child-close', 'file-close', 'unlink'])
97 })
98 test('preexisting receipt is preserved when exclusive creation fails', async (t) => {
99 const state = controls(t, { openError: 'EEXIST' })
100 await assert.rejects(windowsSandboxProbe(env), /EEXIST/)
101 assert.deepEqual(state.events, [])
102 assert.equal(state.removed, false)
103 })
104 test('synchronous spawn failure cleans up only the newly created receipt', async (t) => {
105 const state = controls(t, { spawnThrow: true })
106 await assert.rejects(windowsSandboxProbe(env), /EPERM/)
107 assert.deepEqual(state.events, ['file-close', 'unlink'])
108 })
109 test('asynchronous spawn failure waits for child closure before cleanup', async (t) => {
110 const state = controls(t, { spawnError: true })
111 await assert.rejects(windowsSandboxProbe(env), /EPERM/)
112 assert.deepEqual(state.events, ['kill', 'child-close', 'file-close', 'unlink'])
113 })
114 for (const [name, options] of [
115 ['nonzero exit', { exitCode: 1 }],
116 ['signal exit', { signal: 'SIGTERM' }],
117 ['missing receipt', { output: '' }],
118 ['malformed receipt', { output: '{' }],
119 ['incomplete denial receipt', { output: JSON.stringify({ ...receipt, descendant_denied: false }) }],
120 ['stderr mixed into receipt', { output: `${JSON.stringify(receipt)}\nunexpected warning` }],
121 ['oversized receipt', { output: 'x'.repeat(4097) }],
122 ['receipt growth after stat', { output: 'x'.repeat(4097), statSize: 10 }],
123 ]) {
124 test(`descendant ${name} fails closed and cleans up`, async (t) => {
125 const state = controls(t, options)
126 await assert.rejects(windowsSandboxProbe(env))
127 assert.deepEqual(state.events, ['child-close', 'file-close', 'unlink'])
128 })
129 }
130 test('in-flight excess output terminates the child before its deadline', async (t) => {
131 const state = controls(t, { hang: true, output: 'x'.repeat(4097) })
132 await assert.rejects(windowsSandboxProbe(env), /exceeds 4096 bytes/)
133 assert.deepEqual(state.events, ['kill', 'child-close', 'file-close', 'unlink'])
134 })
135 test('descendant timeout and missing close stay bounded; late close cannot reread a cleaned descriptor', async (t) => {
136 const state = controls(t, { hang: true, hangAfterKill: true })
137 const timeout = globalThis.setTimeout
138 t.after(() => { globalThis.setTimeout = timeout })
139 globalThis.setTimeout = (callback, after, ...args) => timeout(callback, after === 6000 || after === 1000 ? 1 : after, ...args)
140 await assert.rejects(windowsSandboxProbe(env), /descendant probe timed out/)
141 assert.deepEqual(state.events, ['kill', 'file-close', 'unlink'])
142 state.child.emit('close', 0, null)
143 assert.deepEqual(state.positions, [])
144 })
145 test('outside write success is an admission failure', async (t) => {
146 controls(t, { outsideWrite: true })
147 await assert.rejects(windowsSandboxProbe(env), /allowed an outside write/)
148 })
149 test('a missing credential file cannot count as filesystem isolation', async (t) => {
150 controls(t, { readError: 'ENOENT' })
151 await assert.rejects(windowsSandboxProbe(env), /ENOENT/)
152 })
153 test('a reachable loopback connection is an admission failure', async (t) => {
154 controls(t, { networkConnect: true })
155 await assert.rejects(windowsSandboxProbe(env), /allowed direct network/)
156 })
157 test('an unavailable listener cannot count as network isolation', async (t) => {
158 controls(t, { networkError: 'ECONNREFUSED' })
159 await assert.rejects(windowsSandboxProbe(env), /without an access denial: ECONNREFUSED/)
160 })
161 test('network timeout fails admission instead of passing a denial', async (t) => {
162 controls(t, { timeout: true })
163 const timeout = globalThis.setTimeout
164 t.after(() => { globalThis.setTimeout = timeout })
165 globalThis.setTimeout = (callback, after, ...args) => {
166 assert.equal(after, 3000)
167 queueMicrotask(() => callback(...args))
168 return 0
169 }
170 await assert.rejects(windowsSandboxProbe(env), /network probe timed out/)
171 })
172 test('invalid Core projection fails before side effects', async () => {
173 await assert.rejects(windowsSandboxProbe({}), /invalid Core sandbox probe projection/)
174 })
175 test('actual runtime inherits the receipt descriptor and leaves no file; transport evidence only', async (t) => {
176 const root = fs.mkdtempSync(join(tmpdir(), 'cw-win-probe-stdio-'))
177 const inside = join(root, 'inside'), outside = join(root, 'outside')
178 const reads = ['codex', 'dsh', 'builtin'].map((name) => join(root, name))
179 const writeFile = fs.writeFileSync, readFile = fs.readFileSync, connect = net.connect
180 t.after(() => {
181 fs.writeFileSync = writeFile; fs.readFileSync = readFile; net.connect = connect
182 fs.rmSync(root, { recursive: true, force: true })
183 })
184 const denied = () => Object.assign(new Error('synthetic access denial'), { code: 'EACCES' })
185 fs.writeFileSync = (path, ...args) => {
186 if (path === outside) throw denied()
187 return writeFile(path, ...args)
188 }
189 fs.readFileSync = (path, ...args) => {
190 if (reads.includes(path)) throw denied()
191 return readFile(path, ...args)
192 }
193 net.connect = () => {
194 const socket = new EventEmitter()
195 socket.destroy = () => {}
196 process.nextTick(() => socket.emit('error', denied()))
197 return socket
198 }
199 // Only this transport fixture substitutes the child body. Production still
200 // receives Core's same full probe argv; these synthetic denials are not LPAC proof.
201 const args = ['-e', `process.stdout.write(${JSON.stringify(JSON.stringify(receipt) + '\n')})`]
202 assert.deepEqual(await windowsSandboxProbe({ ...env,
203 CODEWHALE_WINDOWS_PROBE_INSIDE: inside, CODEWHALE_WINDOWS_PROBE_OUTSIDE: outside,
204 CODEWHALE_WINDOWS_PROBE_READS: JSON.stringify(reads), CODEWHALE_WINDOWS_PROBE_CHILD_ARGS: JSON.stringify(args),
205 }), receipt)
206 assert.deepEqual(fs.readdirSync(root), [])
207 })
208 test('actual unrestricted Node is rejected by the fixed write control', async () => {
209 const root = fs.mkdtempSync(join(tmpdir(), 'cw-win-probe-negative-'))
210 const inside = join(root, 'inside'), outside = join(root, 'outside')
211 const reads = ['codex', 'dsh', 'builtin'].map((name) => join(root, name))
212 try {
213 reads.forEach((path) => fs.writeFileSync(path, 'non-secret-control'))
214 await assert.rejects(windowsSandboxProbe({ ...env, CODEWHALE_WINDOWS_PROBE_INSIDE: inside,
215 CODEWHALE_WINDOWS_PROBE_OUTSIDE: outside, CODEWHALE_WINDOWS_PROBE_READS: JSON.stringify(reads) }), /allowed an outside write/)
216 assert.equal(fs.existsSync(outside), true, 'real unrestricted write must prove the negative control is exercised')
217 } finally { fs.rmSync(root, { recursive: true, force: true }) }
218 })
219
219 lines Plain Text