Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions build/harness-node-entry.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import childProcess from 'node:child_process'
import net from 'node:net'
import { syncBuiltinESMExports } from 'node:module'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
Expand All @@ -10,6 +11,7 @@ import {
buildPackagedPnpmInvocation
} from './packaged-pnpm-invocation.mjs'
import { enforceWindowsChildProcessHide } from './windows-child-process-hide.mjs'
import { installTypeOfServiceGuard } from './type-of-service-guard.mjs'

// On macOS Harness runs inside an Electron utility process (TCC responsibility
// isolation), so `process.execPath` and `argv0` point at the Electron helper
Expand Down Expand Up @@ -85,6 +87,17 @@ if (process.platform === 'win32') {
process.stdout.write('[harness-node] windowsHide enforcement enabled for child processes\n')
}

// Same reach as the windowsHide enforcement above: the guard patches the
// socket prototype before dshEntryPath loads, so the bundled undici's
// HTTP/1.1 write path — and anything else in this process tree — stops
// dying on a platform EINVAL (issue #821). Self-disables once the bundled
// runtime reports undici >= 7.30.1.
if (installTypeOfServiceGuard(net, process.versions.undici, (message) => process.stderr.write(message))) {
process.stderr.write(
`[harness-node] type-of-service guard enabled (bundled undici ${process.versions.undici})\n`
)
}

if (!dshEntryPath) {
report('startup error', 'missing DSH entry path')
process.exitCode = 1
Expand Down
12 changes: 12 additions & 0 deletions build/type-of-service-guard.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Wrap `Socket.prototype.setTypeOfService` so an EINVAL from the platform
* no longer escapes the undici write path. Returns true when the guard was
* installed (the runtime exposes the API and the bundled undici predates
* undici#6076). `netModule` is the node:net module; typed as unknown because
* the repository's own @types/node may predate `setTypeOfService`.
*/
export function installTypeOfServiceGuard(
netModule: unknown,
undiciVersion: string | undefined,
warn: (message: string) => void
): boolean
55 changes: 55 additions & 0 deletions build/type-of-service-guard.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Node 24's bundled undici (7.29.x) calls socket.setTypeOfService on every
// HTTP/1.1 request write without a try/catch. When a connection is reset in
// the window between connect and the first write, macOS setsockopt(IP_TOS)
// answers EINVAL, the synchronous throw escapes as an uncaught exception,
// and the Harness host dies — entering the plugin market fires enough
// requests to hit it (issue #821; upstream undici#5544, fixed by the 7.x
// backport undici#6076 which no Node 24 release ships yet). The upstream fix
// ignores type-of-service errors entirely, so this guard does the same for
// EINVAL only and installs itself only while the bundled undici predates the
// fix; it self-disables once the runtime reports >= 7.30.1.

const FIXED_UNDICI = [7, 30, 1]

function undiciPredatesFix(undiciVersion) {
const parts = String(undiciVersion ?? '').split('.').map((n) => Number.parseInt(n, 10))
if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) {
return false
}
for (let index = 0; index < FIXED_UNDICI.length; index += 1) {
if (parts[index] < FIXED_UNDICI[index]) return true
if (parts[index] > FIXED_UNDICI[index]) return false
}
return false
}

/**
* Wrap `Socket.prototype.setTypeOfService` so an EINVAL from the platform
* no longer escapes the undici write path. Returns true when the guard was
* installed (bundled undici predates the fix and the API exists).
*/
export function installTypeOfServiceGuard(netModule, undiciVersion, warn) {
const proto = netModule?.Socket?.prototype
if (!proto || typeof proto.setTypeOfService !== 'function' || !undiciPredatesFix(undiciVersion)) {
return false
}
const original = proto.setTypeOfService
let warned = false
proto.setTypeOfService = function setTypeOfServiceGuarded(value) {
try {
return original.call(this, value)
} catch (error) {
if (error && error.code === 'EINVAL') {
if (!warned) {
warned = true
warn(
`[harness-node] ignoring setTypeOfService EINVAL (bundled undici ${undiciVersion} < 7.30.1, undici#5544, issue #821)\n`
)
}
return undefined
}
throw error
}
}
return true
}
56 changes: 56 additions & 0 deletions test/harness-node-entry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,3 +680,59 @@ describe('DSH entry dispatch', () => {
expect(bin).toMatch(/export \{[^}]*\brunCli\b/)
})
})

describe('type-of-service guard (issue #821)', () => {
it('swallows EINVAL from setTypeOfService and warns once', async () => {
const { installTypeOfServiceGuard } = await import('../build/type-of-service-guard.mjs')
let calls = 0
const proto = {
setTypeOfService(_value: number) {
calls += 1
throw Object.assign(new Error('setTypeOfService EINVAL'), { code: 'EINVAL' })
}
}
const warnings: string[] = []
const installed = installTypeOfServiceGuard(
{ Socket: { prototype: proto } },
'7.29.1',
(m: string) => warnings.push(m)
)
expect(installed).toBe(true)
expect(proto.setTypeOfService(0)).toBeUndefined()
expect(proto.setTypeOfService(0)).toBeUndefined()
expect(calls).toBe(2)
expect(warnings).toHaveLength(1)
expect(warnings[0]).toContain('undici#5544')
})

it('rethrows errors other than EINVAL', async () => {
const { installTypeOfServiceGuard } = await import('../build/type-of-service-guard.mjs')
const proto = {
setTypeOfService(_value: number) {
throw Object.assign(new Error('setTypeOfService EPERM'), { code: 'EPERM' })
}
}
installTypeOfServiceGuard({ Socket: { prototype: proto } }, '7.29.1', () => {})
expect(() => proto.setTypeOfService(0)).toThrowError('EPERM')
})

it('does not install when the bundled undici already has the fix', async () => {
const { installTypeOfServiceGuard } = await import('../build/type-of-service-guard.mjs')
const original = () => 1
const proto = { setTypeOfService: original }
expect(installTypeOfServiceGuard({ Socket: { prototype: proto } }, '7.30.1', () => {})).toBe(false)
expect(installTypeOfServiceGuard({ Socket: { prototype: proto } }, '8.1.0', () => {})).toBe(false)
expect(proto.setTypeOfService).toBe(original)
})

it('does not install when the runtime has no setTypeOfService (Node 22 and older)', async () => {
const { installTypeOfServiceGuard } = await import('../build/type-of-service-guard.mjs')
expect(installTypeOfServiceGuard({ Socket: { prototype: {} } }, '7.29.1', () => {})).toBe(false)
// The real net module on this test runner: the guard must be a no-op there too.
const net = (await import('node:net')).default
const before = (net.Socket.prototype as { setTypeOfService?: unknown }).setTypeOfService
const installed = installTypeOfServiceGuard(net, process.versions.undici, () => {})
expect(installed).toBe(false)
expect((net.Socket.prototype as { setTypeOfService?: unknown }).setTypeOfService).toBe(before)
})
})
Loading