| 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 |