返回 CodeWhale
windows-sandbox-probe.mjs
根目录 / crates / tui / extension-host / src / windows-sandbox-probe.mjs
1 // Fixed Core-selected startup diagnostic. No plugin or provider code executes.
2 import fs from 'node:fs'
3 import net from 'node:net'
4 import childProcess from 'node:child_process'
5
6 const denied = (operation) => {
7 try { operation() } catch (error) {
8 if (error?.code === 'EACCES' || error?.code === 'EPERM') return true
9 throw error
10 }
11 return false
12 }
13
14 export async function windowsSandboxProbe(env = process.env) {
15 const inside = env.CODEWHALE_WINDOWS_PROBE_INSIDE
16 const outside = env.CODEWHALE_WINDOWS_PROBE_OUTSIDE
17 const reads = JSON.parse(env.CODEWHALE_WINDOWS_PROBE_READS ?? '[]')
18 const port = Number(env.CODEWHALE_WINDOWS_PROBE_PORT)
19 if (!inside || !outside || reads.length < 3 || !Number.isInteger(port) || port <= 0 || port > 65535) throw new Error('invalid Core sandbox probe projection')
20 const marker = 'codewhale-windows-isolation-probe'
21 fs.writeFileSync(inside, marker, { flag: 'wx' })
22 if (fs.readFileSync(inside, 'utf8') !== marker) throw new Error('sandbox data roundtrip failed')
23 fs.unlinkSync(inside)
24 if (!denied(() => fs.writeFileSync(outside, marker, { flag: 'wx' }))) throw new Error('sandbox allowed an outside write')
25 for (const path of reads) {
26 if (!denied(() => fs.readFileSync(path))) throw new Error('sandbox allowed an outside credential read')
27 }
28 // The Core keeps a real listening socket alive. A timeout, ENOENT, refusal,
29 // or unreachable address cannot be mistaken for network isolation.
30 await new Promise((resolve, reject) => {
31 const socket = net.connect({ host: '127.0.0.1', port })
32 const timer = setTimeout(() => { socket.destroy(); reject(new Error('sandbox network probe timed out')) }, 3000)
33 socket.once('connect', () => { clearTimeout(timer); socket.destroy(); reject(new Error('sandbox allowed direct network')) })
34 socket.once('error', (error) => {
35 clearTimeout(timer); socket.destroy()
36 if (error.code === 'EACCES' || error.code === 'EPERM') resolve()
37 else reject(new Error(`network probe failed without an access denial: ${error.code}`))
38 })
39 })
40 const receipt = { version: 1, data_roundtrip: true, outside_read_denied: true, outside_write_denied: true, network_denied: true, descendant_denied: true }
41 if (env.CODEWHALE_WINDOWS_PROBE_DESCENDANT !== '1') {
42 const args = JSON.parse(env.CODEWHALE_WINDOWS_PROBE_CHILD_ARGS ?? 'null')
43 if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) throw new Error('missing Core child probe argv')
44 // LPAC cannot open NUL or libuv's global named pipes. Core already gave
45 // us an EOF stdin handle; capture the fixed child receipt in own-data and
46 // inherit all three handles instead of asking the runtime to create any.
47 const outputPath = `${inside}.receipt`
48 const fd = fs.openSync(outputPath, 'wx+')
49 try {
50 await new Promise((resolve, reject) => {
51 let child, timer, poll, closeTimer, failure, settled = false
52 const finish = (error) => {
53 if (settled) return
54 settled = true
55 clearTimeout(timer); clearInterval(poll); clearTimeout(closeTimer)
56 if (error) reject(error)
57 else resolve()
58 }
59 const fail = (error) => {
60 if (settled || failure) return
61 failure = error
62 clearTimeout(timer); clearInterval(poll)
63 // Wait for inherited handles to close before cleanup. Core's outer
64 // job deadline still kills the whole tree if termination stalls.
65 closeTimer = setTimeout(() => finish(failure), 1000)
66 try { child.kill() } catch {}
67 }
68 try {
69 child = childProcess.spawn(process.execPath, args, { stdio: [0, fd, fd], windowsHide: true, env: {
70 ...env, CODEWHALE_WINDOWS_PROBE_DESCENDANT: '1', CODEWHALE_WINDOWS_PROBE_INSIDE: `${inside}.child`,
71 } })
72 } catch (error) { finish(error); return }
73 timer = setTimeout(() => fail(new Error('sandbox descendant probe timed out')), 6000)
74 // This stops excess output promptly; it bounds neither a single write
75 // nor disk growth between polls. The fixed diagnostic has no plugin code.
76 poll = setInterval(() => {
77 try {
78 if (fs.fstatSync(fd).size > 4096) fail(new Error('sandbox descendant output exceeds 4096 bytes'))
79 } catch (error) { fail(error) }
80 }, 25)
81 child.once('error', fail)
82 child.once('close', (code, signal) => {
83 if (settled) return
84 if (failure) { finish(failure); return }
85 try {
86 if (code !== 0 || signal) throw new Error('sandbox descendant has no exact denial receipt')
87 if (fs.fstatSync(fd).size > 4096) throw new Error('sandbox descendant output exceeds 4096 bytes')
88 const output = Buffer.alloc(4097)
89 let bytes = 0
90 while (bytes < output.length) {
91 // Inherited file handles share their offset; always read from
92 // the beginning explicitly, including after a short read.
93 const read = fs.readSync(fd, output, bytes, output.length - bytes, bytes)
94 if (read === 0) break
95 bytes += read
96 }
97 if (bytes > 4096) throw new Error('sandbox descendant output exceeds 4096 bytes')
98 if (JSON.stringify(JSON.parse(output.subarray(0, bytes).toString())) !== JSON.stringify(receipt)) throw new Error('sandbox descendant has no exact denial receipt')
99 finish()
100 } catch (error) { finish(error) }
101 })
102 })
103 } finally {
104 try { fs.closeSync(fd) } finally { fs.unlinkSync(outputPath) }
105 }
106 }
107 return receipt
108 }
109
109 lines Plain Text