Skip to content

Commit be9fa4c

Browse files
committed
fix(sea): inject verified native runtimes across platforms
1 parent e5f9cb7 commit be9fa4c

6 files changed

Lines changed: 82 additions & 28 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@
214214
"npm:trust:browser": "node scripts/fleet/registry-infra/npm/settings/trusted-publisher/browser.mts",
215215
"build:sea": "node scripts/repo/cli-build/sea/build.mts",
216216
"check:sea-package": "node scripts/repo/cli-build/sea/check.mts",
217-
"prepack": "pnpm run check:sea-package"
217+
"prepack": "pnpm run check:sea-package",
218+
"test:sea": "node scripts/fleet/test.mts test/repo/unit/sea-package.test.mts"
218219
},
219220
"devDependencies": {
220221
"@anthropic-ai/claude-code": "catalog:",

scripts/repo/cli-build/sea/build.mts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { isMainModule } from '../../../fleet/process/is-main-module.mts'
77
import { runMain } from '../../../fleet/process/run-main.mts'
88
import { BINJECT_VERSION, NODE_SMOL_VERSION } from '../constants/sea-assets.mts'
99
import { fetchSeaAsset } from './assets.mts'
10-
import { extractWindowsSmolRuntime } from './windows-runtime.mts'
10+
import { extractSmolRuntime } from './runtime.mts'
1111
import {
1212
SEA_BUILD_DIR,
1313
SEA_ENTRY_PATH,
14+
SEA_ENTRYPOINT_PATHS,
1415
SEA_LAUNCHER_PATH,
1516
SEA_OUTPUT_DIR,
1617
SEA_PAYLOAD_PATH,
@@ -68,18 +69,26 @@ export async function main(): Promise<void> {
6869
await writeFile(SEA_LAUNCHER_PATH, createSeaLauncher(), { mode: 0o755 })
6970
for (const mode of ['npm', 'npx', 'pnpm', 'yarn']) {
7071
await writeFile(
71-
path.join(path.dirname(SEA_LAUNCHER_PATH), `socket-${mode}.js`),
72+
SEA_ENTRYPOINT_PATHS[`socket-${mode}.js`]!,
7273
`#!/usr/bin/env node\nprocess.env.SOCKET_CLI_MODE = ${JSON.stringify(mode)};\nrequire('./socket.js');\n`,
7374
{ mode: 0o755 },
7475
)
7576
}
77+
const entrypoints: Record<string, string> = {}
78+
for (const [name, file] of Object.entries(SEA_ENTRYPOINT_PATHS)) {
79+
entrypoints[name] = crypto
80+
.createHash('sha256')
81+
.update(await readFile(file))
82+
.digest('hex')
83+
}
7684
await writeFile(
7785
SEA_RECEIPT_PATH,
7886
JSON.stringify(
7987
{
8088
nodeSmol: NODE_SMOL_VERSION,
8189
payload: crypto.createHash('sha256').update(payload).digest('hex'),
8290
binaries: receipt,
91+
entrypoints,
8392
},
8493
null,
8594
2,
@@ -98,12 +107,13 @@ async function buildSeaTarget(
98107
hostBase: string,
99108
): Promise<string> {
100109
const asset = `node-${target.replace('win32-', 'win-')}${target.startsWith('win32-') ? '.exe' : ''}`
101-
let base = await fetchSeaAsset(`node-smol-${NODE_SMOL_VERSION}`, asset)
102-
if (target.startsWith('win32-')) {
103-
const runtime = extractWindowsSmolRuntime(await readFile(base))
104-
base = path.join(SEA_BUILD_DIR, `runtime-${target}.exe`)
105-
await writeFile(base, runtime)
106-
}
110+
const assetPath = await fetchSeaAsset(`node-smol-${NODE_SMOL_VERSION}`, asset)
111+
const runtime = extractSmolRuntime(await readFile(assetPath), target)
112+
const base = path.join(
113+
SEA_BUILD_DIR,
114+
`runtime-${target}${target.startsWith('win32-') ? '.exe' : ''}`,
115+
)
116+
await writeFile(base, runtime, { mode: 0o755 })
107117
const output = seaBinaryPath(target)
108118
const config = path.join(SEA_BUILD_DIR, `${target}.generated.json`)
109119
await writeFile(
@@ -143,7 +153,7 @@ async function buildSeaTarget(
143153
'--sea',
144154
blob,
145155
'--vfs-compat',
146-
...(target.startsWith('win32-') ? ['--skip-repack'] : []),
156+
'--skip-repack',
147157
],
148158
{
149159
stdio: process.argv.includes('--json') ? 'pipe' : 'inherit',

scripts/repo/cli-build/sea/check.mts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { spawn } from '@socketsecurity/lib-stable/process/spawn/child'
44
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
55
import { isMainModule } from '../../../fleet/process/is-main-module.mts'
66
import { runMain } from '../../../fleet/process/run-main.mts'
7-
import { SEA_PAYLOAD_PATH, SEA_RECEIPT_PATH, seaBinaryPath } from './paths.mts'
7+
import {
8+
SEA_ENTRYPOINT_PATHS,
9+
SEA_PAYLOAD_PATH,
10+
SEA_RECEIPT_PATH,
11+
seaBinaryPath,
12+
} from './paths.mts'
813
import { resolveSeaTarget, SEA_TARGETS } from './targets.mts'
914

1015
const logger = getDefaultLogger()
@@ -13,6 +18,7 @@ export async function main(): Promise<void> {
1318
const receipt = JSON.parse(await readFile(SEA_RECEIPT_PATH, 'utf8')) as {
1419
payload: string
1520
binaries: Record<string, string>
21+
entrypoints: Record<string, string>
1622
}
1723
const payload = crypto
1824
.createHash('sha256')
@@ -23,6 +29,17 @@ export async function main(): Promise<void> {
2329
'SEA payload does not match the CLI build. Run pnpm run build:sea.',
2430
)
2531
}
32+
for (const [name, file] of Object.entries(SEA_ENTRYPOINT_PATHS)) {
33+
const actual = crypto
34+
.createHash('sha256')
35+
.update(await readFile(file))
36+
.digest('hex')
37+
if (receipt.entrypoints[name] !== actual) {
38+
throw new Error(
39+
`SEA entry point mismatch for ${name}. Run pnpm run build:sea.`,
40+
)
41+
}
42+
}
2643
const targets = process.argv.includes('--host')
2744
? [
2845
resolveSeaTarget(

scripts/repo/cli-build/sea/paths.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ export function seaBinaryPath(target: string): string {
1717
`socket-${target}${target.startsWith('win32-') ? '.exe' : ''}`,
1818
)
1919
}
20+
21+
export const SEA_ENTRYPOINT_PATHS = Object.fromEntries(
22+
['socket', 'socket-npm', 'socket-npx', 'socket-pnpm', 'socket-yarn'].map(
23+
name => [`${name}.js`, path.join(PACKAGE_ROOT, 'dist', `${name}.js`)],
24+
),
25+
)

scripts/repo/cli-build/sea/windows-runtime.mts renamed to scripts/repo/cli-build/sea/runtime.mts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ const SMOL_METADATA_SIZE = 100
66
const SMOL_CONFIG_SIZE = 1192
77
const MAX_RUNTIME_SIZE = 256 * 1024 * 1024
88

9-
export function extractWindowsSmolRuntime(bytes: Buffer): Buffer {
9+
export function extractSmolRuntime(bytes: Buffer, target: string): Buffer {
1010
const marker = bytes.indexOf(SMOL_MARKER)
1111
if (marker < 0 || marker + SMOL_METADATA_SIZE > bytes.length) {
1212
throw new Error(
13-
'Missing node-smol metadata in Windows base. Verify the pinned asset.',
13+
'Missing node-smol metadata in base. Verify the pinned asset.',
1414
)
1515
}
1616
const compressedLength = Number(bytes.readBigUInt64LE(marker + 32))
@@ -25,7 +25,7 @@ export function extractWindowsSmolRuntime(bytes: Buffer): Buffer {
2525
start + compressedLength > bytes.length
2626
) {
2727
throw new Error(
28-
'Invalid node-smol Windows payload bounds. Verify the pinned asset.',
28+
'Invalid node-smol payload bounds. Verify the pinned asset.',
2929
)
3030
}
3131
const compressed = bytes.subarray(start, start + compressedLength)
@@ -34,18 +34,18 @@ export function extractWindowsSmolRuntime(bytes: Buffer): Buffer {
3434
!crypto.createHash('sha256').update(compressed).digest().equals(expected)
3535
) {
3636
throw new Error(
37-
'Corrupt node-smol Windows compressed payload. Verify the pinned asset.',
37+
'Corrupt node-smol compressed payload. Verify the pinned asset.',
3838
)
3939
}
4040
const runtime = zstdDecompressSync(compressed, {
4141
maxOutputLength: runtimeLength,
4242
})
4343
if (
4444
runtime.length !== runtimeLength ||
45-
runtime.toString('ascii', 0, 2) !== 'MZ'
45+
!hasSmolRuntimeFormat(runtime, target)
4646
) {
4747
throw new Error(
48-
'Invalid node-smol Windows runtime. Expected the declared PE executable size.',
48+
'Invalid node-smol runtime. Expected the declared executable format and size.',
4949
)
5050
}
5151
return runtime
@@ -54,3 +54,13 @@ export function extractWindowsSmolRuntime(bytes: Buffer): Buffer {
5454
function isSmolSize(size: number): boolean {
5555
return Number.isSafeInteger(size) && size > 0 && size <= MAX_RUNTIME_SIZE
5656
}
57+
58+
function hasSmolRuntimeFormat(runtime: Buffer, target: string): boolean {
59+
if (target.startsWith('win32-')) {
60+
return runtime.toString('ascii', 0, 2) === 'MZ'
61+
}
62+
if (target.startsWith('linux-')) {
63+
return runtime.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
64+
}
65+
return runtime.subarray(0, 4).equals(Buffer.from([0xcf, 0xfa, 0xed, 0xfe]))
66+
}

test/repo/unit/sea-package.test.mts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { zstdCompressSync } from 'node:zlib'
2-
import { extractWindowsSmolRuntime } from '../../../scripts/repo/cli-build/sea/windows-runtime.mts'
2+
import { extractSmolRuntime } from '../../../scripts/repo/cli-build/sea/runtime.mts'
33
import { createHash } from 'node:crypto'
44
import { runInNewContext } from 'node:vm'
55
import { describe, expect, it } from 'vitest'
@@ -91,30 +91,40 @@ describe('SEA package', () => {
9191
})
9292
})
9393

94-
describe('Windows node-smol runtime', () => {
94+
describe('node-smol runtime', () => {
9595
it('extracts a verified PE payload', () => {
96-
const fixture = createWindowsFixture()
97-
expect(extractWindowsSmolRuntime(fixture)).toEqual(
96+
const fixture = createSmolFixture()
97+
expect(extractSmolRuntime(fixture, 'win32-arm64')).toEqual(
9898
Buffer.from('MZ example node runtime'),
9999
)
100100
})
101+
it.each([
102+
['linux-x64', [0x7f, 0x45, 0x4c, 0x46]],
103+
['darwin-arm64', [0xcf, 0xfa, 0xed, 0xfe]],
104+
] as const)('extracts %s runtime', (target, magic) => {
105+
const runtime = Buffer.from(magic)
106+
expect(extractSmolRuntime(createSmolFixture(runtime), target)).toEqual(
107+
runtime,
108+
)
109+
})
101110
it('rejects corrupted compressed bytes', () => {
102-
const fixture = createWindowsFixture()
111+
const fixture = createSmolFixture()
103112
fixture[fixture.length - 1] = fixture[fixture.length - 1]! ^ 1
104-
expect(() => extractWindowsSmolRuntime(fixture)).toThrow()
113+
expect(() => extractSmolRuntime(fixture, 'win32-arm64')).toThrow()
105114
})
106115
it('rejects oversized declared output', () => {
107-
const fixture = createWindowsFixture()
116+
const fixture = createSmolFixture()
108117
fixture.writeBigUInt64LE(512n * 1024n * 1024n, 40)
109-
expect(() => extractWindowsSmolRuntime(fixture)).toThrow()
118+
expect(() => extractSmolRuntime(fixture, 'win32-arm64')).toThrow()
110119
})
111120
it('rejects truncated metadata', () => {
112-
expect(() => extractWindowsSmolRuntime(Buffer.alloc(10))).toThrow()
121+
expect(() => extractSmolRuntime(Buffer.alloc(10), 'win32-arm64')).toThrow()
113122
})
114123
})
115124

116-
function createWindowsFixture(): Buffer {
117-
const runtime = Buffer.from('MZ example node runtime')
125+
function createSmolFixture(
126+
runtime = Buffer.from('MZ example node runtime'),
127+
): Buffer {
118128
const compressed = zstdCompressSync(runtime)
119129
const header = Buffer.alloc(100)
120130
header.write('__SMOL_PRESSED_DATA_MAGIC_MARKER')

0 commit comments

Comments
 (0)