fix(ssh): read the macOS machine id from IOPlatformUUID and make the shared-home claim exclusive without hard links (#29259) (#29284)

This commit is contained in:
Neil
2026-10-15 21:22:05 -07:00
committed by GitHub
parent 24b87a7a4d
commit 9c863a6f83
5 changed files with 209 additions and 37 deletions
@@ -3,8 +3,10 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest'
const native = vi.hoisted(() => ({
registry: vi.fn(),
creationTime: vi.fn()
creationTime: vi.fn(),
runProcessSync: vi.fn()
}))
vi.mock('@orca/process-host', () => ({ runProcessSync: native.runProcessSync }))
vi.mock('../../windows-native-registry', () => ({
WINDOWS_REG_SZ: 1,
loadWindowsNativeRegistry: () => ({ HK: { LM: 123 }, getRegistryKey: native.registry })
@@ -21,6 +23,7 @@ beforeEach(() => {
vi.resetModules()
native.registry.mockReset()
native.creationTime.mockReset().mockReturnValue(null)
native.runProcessSync.mockReset()
})
afterEach(() => {
@@ -99,3 +102,36 @@ it.each([
const { profileStateAccessMachineIdentity } = await import('./profile-state-access-identity')
expect(profileStateAccessMachineIdentity()).toBe(expected)
})
const IOREG = `+-o J316sAP <class IOPlatformExpertDevice, id 0x100000253>
{
"IOPlatformSerialNumber" = "C02XXXXXXXXX"
"IOPlatformUUID" = "AAAAAAAA-BBBB-4CCC-8DDD-EEEEEEEEEEEE"
}
`
it('reads the macOS hardware UUID from IOPlatformUUID, not a sysctl macOS lacks', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
native.runProcessSync.mockReturnValue({ code: 0, stdout: IOREG, stderr: '' })
const { profileStateAccessMachineIdentity } = await import('./profile-state-access-identity')
expect(profileStateAccessMachineIdentity()).toBe('aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee')
expect(profileStateAccessMachineIdentity()).toBe('aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee')
expect(native.runProcessSync).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
program: '/usr/sbin/ioreg',
args: ['-rd1', '-c', 'IOPlatformExpertDevice']
})
)
})
it.each([
[{ code: 1, stdout: '', stderr: 'ioreg: failed' }],
[{ code: 0, stdout: '"IOPlatformUUID" = "00000000-0000-0000-0000-000000000000"', stderr: '' }],
[{ code: 0, stdout: '"IOPlatformUUID" = "not-a-uuid"', stderr: '' }],
[{ code: 0, stdout: '', stderr: '' }]
])('keeps an unavailable macOS hardware UUID unverifiable', async (result) => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
native.runProcessSync.mockReturnValue(result)
const { profileStateAccessMachineIdentity } = await import('./profile-state-access-identity')
expect(profileStateAccessMachineIdentity()).toBeNull()
})
@@ -35,13 +35,14 @@ export function profileStateAccessMachineIdentity(): string | null {
? `win32-machine-guid:${value}`
: null
} else if (process.platform === 'darwin') {
// Why not `sysctl kern.hostuuid`: that OID does not exist on macOS (#29259).
const result = runProcessSync({
program: '/usr/sbin/sysctl',
args: ['-n', 'kern.hostuuid'],
program: '/usr/sbin/ioreg',
args: ['-rd1', '-c', 'IOPlatformExpertDevice'],
timeoutMs: 1_000,
maxOutputBytes: 1024
maxOutputBytes: 64 * 1024
})
machineIdentity = result.code === 0 ? result.stdout.trim() || null : null
machineIdentity = result.code === 0 ? parseDarwinPlatformUuid(result.stdout) : null
}
} catch {
// Missing machine identity cannot establish ownership across a reboot.
@@ -49,6 +50,19 @@ export function profileStateAccessMachineIdentity(): string | null {
return machineIdentity
}
/**
* Lowercased so this matches what the SSH machine-home probe reads from the same source
* (`orcad-machine-home.ts`); the two compare owner records against each other.
*/
export function parseDarwinPlatformUuid(ioregOutput: string): string | null {
const value = /"IOPlatformUUID"\s*=\s*"([^"]*)"/u.exec(ioregOutput)?.[1]?.toLowerCase()
return value &&
/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(value) &&
!/^[0-]+$/u.test(value)
? value
: null
}
/** A kernel boot UUID survives hostname changes without conflating machines sharing a profile. */
export function profileStateAccessBootIdentity(): string | null {
if (bootIdentity !== undefined) {
+105 -12
View File
@@ -44,6 +44,7 @@ const { resolveOrcadMachineHome } = await import('./orcad-remote-context')
const {
decideOrcadMachineHome,
orcadMachineIdentity,
ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME,
ORCAD_MACHINE_CLAIM_FILENAME,
ORCAD_MACHINE_HOMES_DIRNAME
} = await import('./orcad-machine-home')
@@ -72,20 +73,54 @@ afterEach(() => {
rmSync(scratch, { recursive: true, force: true })
})
/** The fake machine's hardware UUID, as `sysctl -n kern.hostuuid` answers it. */
/** The fake machine's hardware UUID, as `ioreg` answers its `IOPlatformUUID`. */
function uuidOf(name: string): string {
const hex = Buffer.from(name.padEnd(16, '-')).toString('hex').slice(0, 32)
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
}
function ioregOutput(name: string): string {
return `+-o J316sAP <class IOPlatformExpertDevice>\n "IOPlatformUUID" = "${uuidOf(name).toUpperCase()}"\n`
}
type MachineOptions = {
/** A filesystem without hard links: every `ln` fails. */
noHardLinks?: boolean
/** Holds each machine's write of the claim until both arrive, then delays this one's. */
claimWriteDelay?: number
}
/** A (macOS-flavored) machine sharing `home`, as on an NFS-mounted cluster home. */
function machine(name: string, hostname = name) {
function machine(name: string, hostname = name, options: MachineOptions = {}) {
const bin = join(scratch, `bin-${name}-${hostname}`)
mkdirSync(bin, { recursive: true })
writeFileSync(join(bin, 'uname'), `#!/bin/sh\necho ${hostname}\n`)
writeFileSync(join(bin, 'sysctl'), `#!/bin/sh\necho ${uuidOf(name)}\n`)
chmodSync(join(bin, 'uname'), 0o755)
chmodSync(join(bin, 'sysctl'), 0o755)
const tool = (tool: string, script: string): void => {
writeFileSync(join(bin, tool), `#!/bin/sh\n${script}\n`)
chmodSync(join(bin, tool), 0o755)
}
tool('uname', `echo ${hostname}`)
tool('ioreg', `printf '%s' '${ioregOutput(name)}'`)
// As on macOS, which has no such OID.
tool('sysctl', `echo "sysctl: unknown oid '$2'" >&2; exit 1`)
if (options.noHardLinks) {
tool('ln', 'echo "ln: Operation not supported" >&2; exit 1')
}
if (options.claimWriteDelay !== undefined) {
const barrier = join(scratch, 'claim-write-barrier')
mkdirSync(barrier, { recursive: true })
tool(
'mv',
[
'for last; do :; done',
`case "$last" in */${ORCAD_MACHINE_CLAIM_FILENAME})`,
` : > '${barrier}/'$$; i=0`,
` while [ "$(ls '${barrier}' | wc -l)" -lt 2 ] && [ $i -lt 40 ]; do sleep 0.05; i=$((i + 1)); done`,
` sleep ${options.claimWriteDelay};;`,
'esac',
'exec /bin/mv "$@"'
].join('\n')
)
}
const env = {
...process.env,
HOME: home,
@@ -106,7 +141,7 @@ function claimPath(): string {
return join(home, '.orca-remote', ORCAD_MACHINE_CLAIM_FILENAME)
}
function legacyOwner(name: string): void {
function legacyOwner(name: string, machineIdentity: string | null = uuidOf(name)): void {
const dir = join(home, '.orca', '.profile-state-access', 'participants', 'token')
mkdirSync(dir, { recursive: true })
writeFileSync(
@@ -116,7 +151,7 @@ function legacyOwner(name: string): void {
pid: 4242,
host: name,
platform: 'darwin',
machineIdentity: uuidOf(name)
...(machineIdentity === null ? {} : { machineIdentity })
})
)
}
@@ -209,6 +244,58 @@ describe.runIf(POSIX)('resolveOrcadMachineHome on a home several machines share'
)
})
it('keeps a Mac whose orcad predates macOS machine identity on its running install', async () => {
const a = machine('node-a')
mkdirSync(join(home, '.orca-remote', 'orcad-0.1.0+abc'), { recursive: true })
// Older orcads on macOS recorded no machine identity at all.
legacyOwner('node-a', null)
expect(await resolveOrcadMachineHome(a, true)).toEqual(loginHomeResult())
expect(await resolveOrcadMachineHome(a, true)).toEqual(loginHomeResult())
})
it('lets exactly one machine claim a home on a filesystem without hard links', async () => {
const a = machine('node-a', 'node-a', { noHardLinks: true, claimWriteDelay: 0 })
const b = machine('node-b', 'node-b', { noHardLinks: true, claimWriteDelay: 0.5 })
const homes = await Promise.all([
resolveOrcadMachineHome(a, true),
resolveOrcadMachineHome(b, true)
])
expect(homes.filter((resolved) => resolved.orcadHome === home)).toHaveLength(1)
expect(readdirNames(join(home, '.orca-remote'))).toEqual(
[
ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME,
ORCAD_MACHINE_CLAIM_FILENAME,
ORCAD_MACHINE_HOMES_DIRNAME
].sort()
)
expect(readdirNames(join(home, '.orca-remote', ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME))).toEqual([
ORCAD_MACHINE_CLAIM_FILENAME
])
// Stable on later connects.
expect(
await Promise.all([resolveOrcadMachineHome(a, true), resolveOrcadMachineHome(b, true)])
).toEqual(homes)
})
it('lets exactly one of two machines replace an empty claim', async () => {
mkdirSync(join(home, '.orca-remote'))
writeFileSync(claimPath(), '')
const homes = await Promise.all([
resolveOrcadMachineHome(machine('node-a', 'node-a', { claimWriteDelay: 0 }), true),
resolveOrcadMachineHome(machine('node-b', 'node-b', { claimWriteDelay: 0.5 }), true)
])
expect(homes.filter((resolved) => resolved.orcadHome === home)).toHaveLength(1)
})
it('restores an emptied claim to the machine the arbiter already chose', async () => {
const a = machine('node-a', 'node-a', { noHardLinks: true })
const b = machine('node-b', 'node-b', { noHardLinks: true })
expect(await resolveOrcadMachineHome(a, true)).toEqual(loginHomeResult())
writeFileSync(claimPath(), '')
expect((await resolveOrcadMachineHome(b, true)).orcadHome).not.toBe(home)
expect(await resolveOrcadMachineHome(a, true)).toEqual(loginHomeResult())
})
it('keeps a renamed machine on its own install', async () => {
expect(await resolveOrcadMachineHome(machine('node-a'), true)).toEqual(loginHomeResult())
expect(await resolveOrcadMachineHome(machine('node-a', 'renamed'), true)).toEqual(
@@ -265,12 +352,18 @@ describe('decideOrcadMachineHome', () => {
it('keys macOS by hardware UUID alone, since its hostname follows the network', () => {
const uuid = '12345678-1234-1234-1234-123456789ABC'
expect(orcadMachineIdentity(darwin, uuid, 'cafe-wifi')?.key).toBe(
orcadMachineIdentity(darwin, uuid, 'home-wifi')?.key
const ioreg = ` "IOPlatformUUID" = "${uuid}"`
expect(orcadMachineIdentity(darwin, ioreg, 'cafe-wifi')?.key).toBe(
orcadMachineIdentity(darwin, ioreg, 'home-wifi')?.key
)
expect(orcadMachineIdentity(darwin, ioreg, 'cafe-wifi')?.machineIdentity).toBe(
uuid.toLowerCase()
)
// What `sysctl -n kern.hostuuid` printed on macOS: nothing.
expect(orcadMachineIdentity(darwin, '', 'cafe-wifi')).toBeNull()
const renamed = probe(darwin, {
identity: orcadMachineIdentity(darwin, uuid, 'cafe-wifi'),
dataRootOwners: [{ host: 'home-wifi', machineIdentity: uuid }]
identity: orcadMachineIdentity(darwin, ioreg, 'cafe-wifi'),
dataRootOwners: [{ host: 'home-wifi', machineIdentity: uuid.toLowerCase() }]
})
expect(decideOrcadMachineHome(renamed, darwin).kind).toBe('claim')
})
+31 -20
View File
@@ -13,6 +13,7 @@
import { createHash, randomUUID } from 'node:crypto'
import { shellEscape } from './ssh-connection-utils'
import { RELAY_REMOTE_DIR } from './relay-protocol'
import { parseDarwinPlatformUuid } from '../persistence/profile-state/profile-state-access-identity'
import {
joinRemotePath,
normalizeRemoteHome,
@@ -25,6 +26,8 @@ export const ORCAD_MACHINE_CLAIM_FILENAME = 'orcad-machine.json'
/** Under `~/.orca-remote`: one orcad home per other machine sharing this `$HOME`. */
export const ORCAD_MACHINE_HOMES_DIRNAME = 'hosts'
export const ORCAD_MACHINE_CLAIM_SCHEMA_VERSION = 1
/** Under `~/.orca-remote`: decides the claim when hard links cannot (see the claim command). */
export const ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME = 'orcad-machine.arbiter'
const SECTION = '__ORCA_MACHINE_HOME_SECTION__'
const CLAIM_ABSENT = 'ABSENT'
@@ -74,7 +77,9 @@ export function orcadMachineHomeProbeCommand(host: RemoteHostPlatform): string {
return [
`printf '%s\\n' "$HOME"; echo ${SECTION};`,
host.os === 'darwin'
? 'sysctl -n kern.hostuuid 2>/dev/null;'
? // Bare first so a test (or a host that moved it) can supply its own ioreg.
'{ ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null ||' +
' /usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null; } | grep IOPlatformUUID;'
: 'cat /etc/machine-id 2>/dev/null;',
`echo; echo ${SECTION}; uname -n 2>/dev/null; echo; echo ${SECTION};`,
// An empty claim is one a full disk cut short; the claim command replaces it.
@@ -128,9 +133,7 @@ export function orcadMachineIdentity(
): OrcadMachineIdentity | null {
const machineIdentity =
host.os === 'darwin'
? /^[0-9A-Fa-f-]{36}$/u.test(machineRaw)
? machineRaw.toLowerCase()
: null
? parseDarwinPlatformUuid(machineRaw)
: /^[a-f0-9]{32}$/u.test(machineRaw) && !/^0+$/u.test(machineRaw)
? machineRaw
: null
@@ -266,26 +269,29 @@ export function orcadMachineDataRoot(
/**
* Publishes the claim exclusively and prints whichever claim won. Why a hard link: `ln` is atomic
* and exclusive on NFS too, and a reader never sees a half-written claim. A filesystem without
* hard links, or an empty claim a full disk left, falls back to an atomic (not exclusive) rename.
* and exclusive on NFS too, and a reader never sees a half-written claim. Where `ln` cannot decide
* (no hard links, or an empty claim a full disk left), a directory rename picks the winner: it is
* atomic everywhere, and the arbiter arrives already holding the winner's claim, so a crash leaves
* no half-taken lock. Every machine then republishes that same claim, so concurrent writes agree.
*/
export function orcadMachineClaimCommand(
host: RemoteHostPlatform,
loginHome: string,
machineKey: string
): string {
const base = shellEscape(joinRemotePath(host, loginHome, RELAY_REMOTE_DIR))
const claim = shellEscape(
joinRemotePath(host, loginHome, RELAY_REMOTE_DIR, ORCAD_MACHINE_CLAIM_FILENAME)
)
const partial = shellEscape(
joinRemotePath(
host,
loginHome,
RELAY_REMOTE_DIR,
`${ORCAD_MACHINE_CLAIM_FILENAME}.partial.${randomUUID()}`
)
)
const remoteDir = (...parts: string[]): string =>
shellEscape(joinRemotePath(host, loginHome, RELAY_REMOTE_DIR, ...parts))
const attempt = randomUUID()
const base = remoteDir()
const claim = remoteDir(ORCAD_MACHINE_CLAIM_FILENAME)
const partial = remoteDir(`${ORCAD_MACHINE_CLAIM_FILENAME}.partial.${attempt}`)
const stageName = `${ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME}.partial.${attempt}`
const stage = remoteDir(stageName)
const stageClaim = remoteDir(stageName, ORCAD_MACHINE_CLAIM_FILENAME)
const arbiter = remoteDir(ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME)
const arbiterClaim = remoteDir(ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME, ORCAD_MACHINE_CLAIM_FILENAME)
// `mv` into an existing arbiter nests the stage inside it instead of replacing it.
const nestedStage = remoteDir(ORCAD_MACHINE_CLAIM_ARBITER_DIRNAME, stageName)
const body = shellEscape(
JSON.stringify({
schemaVersion: ORCAD_MACHINE_CLAIM_SCHEMA_VERSION,
@@ -295,7 +301,12 @@ export function orcadMachineClaimCommand(
return [
`umask 077; mkdir -p ${base} || exit 65;`,
`printf %s ${body} > ${partial} || exit 65;`,
`ln ${partial} ${claim} 2>/dev/null || [ -s ${claim} ] || mv -f ${partial} ${claim};`,
`rm -f ${partial}; cat ${claim}`
// Once an arbiter exists it decides any claim that later goes missing or empty.
`if { [ -d ${arbiter} ] || ! ln ${partial} ${claim} 2>/dev/null; } && [ ! -s ${claim} ]; then`,
`{ mkdir ${stage} && printf %s ${body} > ${stageClaim}; } || { rm -rf ${stage} ${partial}; exit 65; };`,
`mv ${stage} ${arbiter} 2>/dev/null; rm -rf ${stage} ${nestedStage};`,
`[ -s ${arbiterClaim} ] && cat ${arbiterClaim} > ${partial} || { rm -f ${partial}; exit 65; };`,
`[ -s ${claim} ] || mv -f ${partial} ${claim};`,
`fi; rm -f ${partial}; cat ${claim}`
].join(' ')
}
@@ -47,6 +47,24 @@ describe('classifyRuntimeHostRePair', () => {
).toEqual({ kind: 'same-host', pin: next, descriptor: 'moved' })
})
it('does not call a macOS host moved when an upgrade first adds its binding, or a downgrade drops it', () => {
// Older Orca read no macOS machine id, so its descriptor carried no binding (#29259).
const unbound = { installationId: INSTALL_A }
const bound = { installationId: INSTALL_A, machineBinding: BIND_1 }
expect(
classifyRuntimeHostRePair(
{ publicKeys: ['key-a'], pin: unbound },
{ publicKeyB64: 'key-a', hostDescriptor: bound }
)
).toEqual({ kind: 'same-host', pin: bound, descriptor: 'pinned' })
expect(
classifyRuntimeHostRePair(
{ publicKeys: ['key-a'], pin: bound },
{ publicKeyB64: 'key-a', hostDescriptor: unbound }
)
).toEqual({ kind: 'same-host', pin: unbound, descriptor: 'pinned' })
})
it('never learns from a downgraded server that stopped sending a descriptor', () => {
const pin = { installationId: INSTALL_A, machineBinding: BIND_1 }
expect(