Repository navigation
fix(scheduler): block unexecuted xdt-node pre-run checks #6237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shenjiacheng-sudo
wants to merge
8
commits into
makecindy:main
Choose a base branch
from
shenjiacheng-sudo:cindy/sweet-stallman
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b1a5c85
fix(scheduler): block unexecuted xdt-node pre-run checks
987654321-master 50bf12c
fix(scheduler): isolate hook startup proof from workers and shell red…
987654321-master 190c841
fix(scheduler): isolate pre-run startup proof from permission and pre…
987654321-master f83c6b0
fix(scheduler): localize startup verification failures and probe perm…
987654321-master e15be97
fix(scheduler): bound Unix startup receipt paths
987654321-master cb6eee7
fix(scheduler): attest after user ESM preloads
987654321-master b83d8a7
fix(scheduler): verify gate completion with native Node supervisor
987654321-master 4e43803
fix(scheduler): sign and isolate packaged pre-run Node
987654321-master File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { chmodSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { nodeRuntimeArtifact } from './src/main/managed-tools/nodeArtifact'; | ||
| import { extractToolArchive } from './src/main/managed-tools/archive'; | ||
| import { downloadToFileWithTimeout } from '../../tools/shared/fetch-with-timeout.mjs'; | ||
| import { assertSha256, sha256File } from '../../tools/shared/verify-sha256.mjs'; | ||
|
|
||
| /** Only prune the package's copy, never the shared multi-target source cache. | ||
| * Validate the complete target before deleting stale generations/other hosts. */ | ||
| export function prunePackagedPreRunNode(resources: string, platform: string, arch: string): void { | ||
| const artifact = nodeRuntimeArtifact(platform, arch); | ||
| if (!artifact) throw new Error('Unsupported packaged pre-run Node target: ' + platform + '-' + arch); | ||
| const root = path.join(resources, 'tools', 'pre-run-node'); | ||
| const target = path.join(root, artifact.host); | ||
| try { | ||
| if (!lstatSync(root).isDirectory() || !lstatSync(target).isDirectory()) throw new Error('Target is not a directory'); | ||
| for (const file of [platform === 'win32' ? 'node.exe' : 'node', platform === 'win32' ? 'supervisor.exe' : 'supervisor', 'gate-client.cjs', 'NODE-LICENSE.txt', 'manifest.json']) { | ||
| const stat = lstatSync(path.join(target, file)); | ||
| if (!stat.isFile() || stat.size === 0) throw new Error('Missing or empty ' + file); | ||
| } | ||
| const manifest = JSON.parse(readFileSync(path.join(target, 'manifest.json'), 'utf8')); | ||
| if (manifest.host !== artifact.host || manifest.version !== artifact.version || manifest.archiveSha256 !== artifact.sha256) { | ||
| throw new Error('Target manifest does not match the pinned artifact'); | ||
| } | ||
| } catch (error) { | ||
| throw new Error('Incomplete packaged pre-run Node: ' + artifact.host, { cause: error }); | ||
| } | ||
| for (const entry of readdirSync(root)) { | ||
| if (entry !== artifact.host) rmSync(path.join(root, entry), { recursive: true, force: true }); | ||
| } | ||
| } | ||
|
|
||
| /** Small native helper, no Cargo/npm install or runtime download. */ | ||
| export function compilePreRunSupervisor(source: string, output: string, platform: string = process.platform, arch: string = process.arch): void { | ||
| if (platform !== process.platform || (platform !== 'darwin' && arch !== process.arch)) { | ||
| throw new Error('Pre-run supervisor requires a native target build'); | ||
| } | ||
| mkdirSync(path.dirname(output), { recursive: true }); | ||
| if (platform === 'win32') { | ||
| const object = output + '.obj'; | ||
| try { | ||
| execFileSync('cl.exe', [ | ||
| '/nologo', '/std:c11', '/W4', '/O2', '/MT', '/Fo' + object, '/Fe' + output, | ||
| source, '/link', 'advapi32.lib', | ||
| ], { stdio: 'pipe', windowsHide: true }); | ||
| } finally { rmSync(object, { force: true }); } | ||
| } | ||
| else execFileSync('cc', [ | ||
| '-std=c11', '-Wall', '-Wextra', '-Werror', '-O2', '-pthread', | ||
| ...(platform === 'darwin' ? ['-arch', arch === 'arm64' ? 'arm64' : 'x86_64'] : []), | ||
| source, '-o', output, | ||
| ], { stdio: 'pipe' }); | ||
| } | ||
|
|
||
| /** Package-time preparation only. Verified publisher archive, isolated stage, | ||
| * upstream full LICENSE, then a complete generation replaces the target. */ | ||
| export async function stagePreRunNode(platform: string, arch: string, options: { | ||
| desktopRoot?: string; outputRoot?: string; archive?: string; download?: boolean; | ||
| } = {}): Promise<void> { | ||
| const artifact = nodeRuntimeArtifact(platform, arch); | ||
| if (!artifact) throw new Error('Unsupported packaged pre-run Node target: ' + platform + '-' + arch); | ||
| const desktop = options.desktopRoot ?? __dirname; | ||
| const outputRoot = options.outputRoot ?? path.join(desktop, 'resources', 'tools', 'pre-run-node'); | ||
| const scratch = mkdtempSync(path.join(os.tmpdir(), 'cindy-pre-run-build-')); | ||
| const target = path.join(outputRoot, artifact.host); | ||
| const staged = path.join(outputRoot, '.' + artifact.host + '-' + path.basename(scratch)); | ||
| const old = staged + '-old'; | ||
| let movedOld = false; | ||
| try { | ||
| mkdirSync(staged, { recursive: true }); | ||
| const archive = options.archive ?? path.join(scratch, 'node.' + artifact.format); | ||
| if (!options.archive) { | ||
| if (options.download === false) throw new Error('Verified Node archive required for offline preparation'); | ||
| await downloadToFileWithTimeout(artifact.url, archive, {}, { minThroughputBytesPerSec: 0 }); | ||
| } | ||
| assertSha256({ actualHex: sha256File(archive), expected: artifact.sha256, label: artifact.host + ' Node archive' }); | ||
| const extracted = path.join(scratch, 'extracted'); mkdirSync(extracted); | ||
| await extractToolArchive(archive, extracted, artifact, new AbortController().signal); | ||
| const node = path.join(extracted, ...artifact.executable.split('/')); | ||
| const executable = path.join(staged, platform === 'win32' ? 'node.exe' : 'node'); | ||
| copyFileSync(node, executable); chmodSync(executable, 0o755); | ||
|
shenjiacheng-sudo marked this conversation as resolved.
|
||
| copyFileSync(path.join(extracted, artifact.executable.split('/')[0], 'LICENSE'), path.join(staged, 'NODE-LICENSE.txt')); | ||
| copyFileSync(path.join(desktop, 'native', 'pre-run-node', 'gate-client.cjs'), path.join(staged, 'gate-client.cjs')); | ||
| compilePreRunSupervisor(path.join(desktop, 'native', 'pre-run-node', 'supervisor.c'), path.join(staged, platform === 'win32' ? 'supervisor.exe' : 'supervisor'), platform, arch); | ||
| writeFileSync(path.join(staged, 'manifest.json'), JSON.stringify({ version: artifact.version, archiveSha256: artifact.sha256, host: artifact.host }) + '\n'); | ||
| // Never expose a partially built directory or destroy the prior good stage. | ||
| try { renameSync(target, old); movedOld = true; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } | ||
| try { renameSync(staged, target); } catch (error) { if (movedOld) renameSync(old, target); throw error; } | ||
| if (movedOld) rmSync(old, { recursive: true, force: true }); | ||
| } finally { | ||
| rmSync(staged, { recursive: true, force: true }); rmSync(scratch, { recursive: true, force: true }); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| 'use strict'; | ||
| /** Cooperative gate-body protocol. Loading this module never attests entry. | ||
| * Call enter() in the check body, complete(code) after its work, then exit. | ||
| * This is not an attestation mechanism against hostile same-process code. */ | ||
| const { createConnection } = require('node:net'); | ||
| const { isMainThread } = require('node:worker_threads'); | ||
| let socket, pending, failure, entered = false, completed = false, response = ''; | ||
| function frame(verb, code) { | ||
| if (failure || !socket || socket.destroyed) return Promise.reject(failure || new Error('Gate protocol is closed')); | ||
| if (pending) return Promise.reject(new Error('Concurrent pre-run gate protocol call')); | ||
| return new Promise((resolve, reject) => { | ||
| pending = { resolve, reject }; | ||
| socket.write(verb + ' ' + process.env.CINDY_PRE_RUN_GATE_NONCE + (code === undefined ? '' : ' ' + code) + '\n'); | ||
| }); | ||
| } | ||
| async function enter() { | ||
| if (!isMainThread || entered || completed) throw new Error('Gate entry must occur once in the root main thread'); | ||
| const endpoint = process.env.CINDY_PRE_RUN_GATE_ENDPOINT; | ||
| if (!endpoint || !process.env.CINDY_PRE_RUN_GATE_NONCE) throw new Error('Gate is not running under the Cindy pre-run supervisor'); | ||
| socket = createConnection(endpoint); | ||
| socket.setEncoding('utf8'); | ||
| socket.on('data', chunk => { | ||
| response += chunk; | ||
| if (response.length > 16) { socket.destroy(new Error('Invalid gate acknowledgement')); return; } | ||
| if (response.endsWith('\n')) { | ||
| const operation = pending; pending = undefined; | ||
| if (!operation) { socket.destroy(new Error('Unexpected gate acknowledgement')); return; } | ||
| const ok = response === 'ok\n'; response = ''; | ||
| if (ok) operation.resolve(); else operation.reject(new Error('Gate proof rejected')); | ||
| } | ||
| }); | ||
| socket.on('error', error => { failure = error; const operation = pending; pending = undefined; operation?.reject(error); }); | ||
| socket.on('close', () => { failure ??= new Error('Gate protocol closed before acknowledgement'); const operation = pending; pending = undefined; operation?.reject(failure); }); | ||
| await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); | ||
| await frame('ENTER'); entered = true; | ||
| } | ||
| async function complete(code) { | ||
| if (!entered || completed || ![0, 2].includes(code)) throw new Error('Gate must enter and complete once with 0 or 2'); | ||
| await frame('COMPLETE', code); completed = true; process.exitCode = code; | ||
| await new Promise((resolve, reject) => { socket.once('close', resolve); socket.once('error', reject); socket.end(); }); | ||
| } | ||
| async function exit(code) { await complete(code); process.exit(code); } | ||
| module.exports = { enter, complete, exit }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>com.apple.security.cs.allow-jit</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.