Files
orca/config/scripts/verify-linux-glibc-floor.test.mjs
T
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00

475 lines
20 KiB
JavaScript

import { mkdtemp, mkdir, writeFile, symlink, rm } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const {
readElfMachine,
declaredArchFromPath,
findArchViolation,
parseGlibcVersion,
compareGlibcVersions,
parseVersionNeeds,
parseNeededLibraries,
parseImportedSymbols,
isVersionNodeAboveFloor,
findFloorViolations,
DESKTOP_GLIBC_FLOOR,
SERVER_SLOT_GLIBC_FLOOR,
COMPAT_SLOT_GLIBC_FLOOR,
findMissingProviderDeps,
collectNativeBinaries,
verifyLinuxGlibcFloor
} = require('./verify-linux-glibc-floor.cjs')
// 0x7f 'E' 'L' 'F' + class/data/version padding — enough for the magic check.
const ELF_HEADER = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00])
// Model objdump -p version rows with weak 0x02 needs, symbol-less ABI marker and C++ requirement.
const OBJDUMP_P = [
'Dynamic Section:',
' NEEDED libc.so.6',
'',
'Version References:',
' required from libc.so.6:',
' 0x09691a75 0x00 06 GLIBC_2.2.5',
' 0x069691b4 0x00 05 GLIBC_2.34',
' 0x0d696914 0x02 04 GLIBC_2.18',
' 0x00fd0e42 0x00 03 GLIBC_ABI_DT_RELR',
' required from libstdc++.so.6:',
' 0x0b481abc 0x00 07 GLIBCXX_3.4.29',
''
].join('\n')
describe('verify-linux-glibc-floor parsing', () => {
it('parses and compares numeric version tuples', () => {
expect(parseGlibcVersion('2.34')).toEqual([2, 34])
expect(parseGlibcVersion('3.4.28')).toEqual([3, 4, 28])
expect(compareGlibcVersions([2, 2, 5], [2, 14])).toBe(-1)
expect(compareGlibcVersions([2, 31], [2, 32])).toBe(-1)
expect(compareGlibcVersions([2, 34], [2, 31])).toBe(1)
expect(compareGlibcVersions([2, 31], [2, 31])).toBe(0)
expect(compareGlibcVersions([2, 31], [2, 31, 0])).toBe(0)
expect(compareGlibcVersions([3, 4, 29], [3, 4, 28])).toBe(1)
})
it('parses objdump -p Version References into per-library version needs', () => {
const needs = parseVersionNeeds(OBJDUMP_P)
expect(needs).toContainEqual({ library: 'libc.so.6', name: 'GLIBC_2.34', weak: false })
expect(needs).toContainEqual({ library: 'libc.so.6', name: 'GLIBC_ABI_DT_RELR', weak: false })
expect(needs).toContainEqual({ library: 'libc.so.6', name: 'GLIBC_2.18', weak: true })
expect(needs).toContainEqual({ library: 'libstdc++.so.6', name: 'GLIBCXX_3.4.29', weak: false })
})
it('classifies version nodes across glibc and libstdc++ families', () => {
expect(isVersionNodeAboveFloor('GLIBC_2.34')).toBe(true)
expect(isVersionNodeAboveFloor('GLIBC_2.31')).toBe(false)
expect(isVersionNodeAboveFloor('GLIBC_ABI_DT_RELR')).toBe(true) // Reject GLIBC_PRIVATE even if version node exists: private symbol may be absent on floor, like newer ABI marker.
expect(isVersionNodeAboveFloor('GLIBC_PRIVATE')).toBe(true)
expect(isVersionNodeAboveFloor('CXXABI_TM_1')).toBe(false) // named libstdc++ node on 20.04
expect(isVersionNodeAboveFloor('GLIBCXX_3.4.29')).toBe(true) // GCC 11, above 20.04's 3.4.28
expect(isVersionNodeAboveFloor('GLIBCXX_3.4.28')).toBe(false)
expect(isVersionNodeAboveFloor('CXXABI_1.3.13')).toBe(true)
expect(isVersionNodeAboveFloor('CXXABI_1.3.12')).toBe(false)
expect(isVersionNodeAboveFloor('GCC_3.0')).toBe(false) // family not gated
})
it('flags strong too-new glibc + libstdc++ needs, skipping weak and ungated families', () => {
const violations = findFloorViolations(parseVersionNeeds(OBJDUMP_P), '/opt/app/pty.node')
const names = violations.map((v) => v.name).sort()
// Reject strong too-new requirements; omit weak GLIBC_2.18 and supported GLIBC_2.2.5.
expect(names).toEqual(['GLIBCXX_3.4.29', 'GLIBC_2.34', 'GLIBC_ABI_DT_RELR'].sort())
})
it('exempts sherpa-onnx from the libstdc++ floor but still gates its glibc', () => {
const needs = [
{ library: 'libstdc++.so.6', name: 'GLIBCXX_3.4.29', weak: false },
{ library: 'libc.so.6', name: 'GLIBC_2.34', weak: false }
]
// A launch-critical module: both are violations.
expect(
findFloorViolations(needs, '/opt/app/node_modules/node-pty/pty.node').map((v) => v.name)
).toEqual(['GLIBCXX_3.4.29', 'GLIBC_2.34'])
// sherpa: GLIBCXX exempt (lazy speech prebuilt), glibc still enforced.
expect(
findFloorViolations(
needs,
'/opt/app/node_modules/sherpa-onnx-linux-x64/sherpa-onnx.node'
).map((v) => v.name)
).toEqual(['GLIBC_2.34'])
})
it('reports no violations when every strong need is at or below the floor', () => {
const needs = parseVersionNeeds(
[
'Version References:',
' required from libc.so.6:',
' 0x00 0x00 02 GLIBC_2.2.5',
' 0x00 0x00 03 GLIBC_2.28',
' required from libstdc++.so.6:',
' 0x00 0x00 04 GLIBCXX_3.4.22'
].join('\n')
)
expect(findFloorViolations(needs, '/opt/app/pty.node')).toEqual([])
})
})
describe('floor profiles', () => {
const needs = (...names) => names.map((name) => ({ library: 'libc.so.6', name, weak: false }))
const flagged = (list, floor) =>
findFloorViolations(list, '/opt/orcad/pty.node', floor).map((v) => v.name)
it('keeps the desktop default at Ubuntu 20.04', () => {
expect(DESKTOP_GLIBC_FLOOR.label).toContain('glibc 2.31')
expect(flagged(needs('GLIBC_2.31', 'GLIBC_2.32'))).toEqual(['GLIBC_2.32'])
expect(flagged(needs('GLIBC_2.31', 'GLIBC_2.32'), DESKTOP_GLIBC_FLOOR)).toEqual(['GLIBC_2.32'])
})
it('gates server slots at glibc 2.28 and GCC 8 libstdc++', () => {
expect(
flagged(
needs(
'GLIBC_2.28',
'GLIBC_2.29',
'GLIBCXX_3.4.25',
'GLIBCXX_3.4.26',
'CXXABI_1.3.11',
'CXXABI_1.3.12'
),
SERVER_SLOT_GLIBC_FLOOR
)
).toEqual(['GLIBC_2.29', 'GLIBCXX_3.4.26', 'CXXABI_1.3.12'])
})
it('gates the compat slot at glibc 2.17 and GCC 4.8 libstdc++', () => {
expect(
flagged(
needs('GLIBC_2.17', 'GLIBC_2.18', 'GLIBCXX_3.4.19', 'GLIBCXX_3.4.20', 'CXXABI_1.3.8'),
COMPAT_SLOT_GLIBC_FLOOR
)
).toEqual(['GLIBC_2.18', 'GLIBCXX_3.4.20', 'CXXABI_1.3.8'])
})
it('still rejects non-numeric glibc nodes under every profile', () => {
for (const floor of [SERVER_SLOT_GLIBC_FLOOR, COMPAT_SLOT_GLIBC_FLOOR]) {
expect(flagged(needs('GLIBC_ABI_DT_RELR', 'GLIBC_PRIVATE'), floor)).toEqual([
'GLIBC_ABI_DT_RELR',
'GLIBC_PRIVATE'
])
}
})
})
describe('DT_NEEDED provider check', () => {
const OBJDUMP_P_DYNAMIC = [
'Dynamic Section:',
' NEEDED libutil.so.1',
' NEEDED libpthread.so.0',
' NEEDED libc.so.6',
'',
'Version References:',
' required from libc.so.6:',
' 0x0 0x00 02 GLIBC_2.2.5'
].join('\n')
it('parses DT_NEEDED shared libraries from objdump -p', () => {
const needed = parseNeededLibraries(OBJDUMP_P_DYNAMIC)
expect([...needed].sort()).toEqual(['libc.so.6', 'libpthread.so.0', 'libutil.so.1'])
})
it('parses undefined imported symbols from objdump -T, stripping @VERSION', () => {
const output = [
'0000000000000000 DF *UND*\t0000000000000000 (GLIBC_2.2.5) openpty',
'0000000000000000 w DF *UND*\t0000000000000000 __cxa_finalize@GLIBC_2.2.5',
'0000000000000000 DF .text\t0000000000000000 defined_symbol'
].join('\n')
const imported = parseImportedSymbols(output)
expect(imported.has('openpty')).toBe(true)
expect(imported.has('__cxa_finalize')).toBe(true)
expect(imported.has('defined_symbol')).toBe(false) // not *UND*
})
it('flags a binary that imports openpty/forkpty without libutil.so.1 in DT_NEEDED', () => {
const importsPty = new Set(['openpty', 'forkpty', 'free'])
// Missing libutil.so.1 -> the pinned symbols would not resolve on the floor.
expect(
findMissingProviderDeps(importsPty, new Set(['libc.so.6'])).map((m) => m.symbol)
).toEqual(['openpty', 'forkpty'])
// With libutil.so.1 present, no violation.
expect(findMissingProviderDeps(importsPty, new Set(['libc.so.6', 'libutil.so.1']))).toEqual([])
// A binary that doesn't import the relocated symbols is never flagged.
expect(findMissingProviderDeps(new Set(['free']), new Set(['libc.so.6']))).toEqual([])
})
})
describe('collectNativeBinaries', () => {
it('collects only ELF .node/.so/executable files, skipping non-ELF and symlinks', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-collect-'))
try {
await mkdir(join(root, 'nested'), { recursive: true })
await writeFile(join(root, 'addon.node'), ELF_HEADER)
await writeFile(join(root, 'nested', 'lib.so'), ELF_HEADER)
await writeFile(join(root, 'nested', 'lib.so.1'), ELF_HEADER)
await writeFile(join(root, 'orca-ide'), ELF_HEADER) // extensionless executable
await writeFile(join(root, 'script.js'), ELF_HEADER) // has extension, not native
await writeFile(join(root, 'text.node'), 'not an elf file') // native name, non-ELF
await writeFile(join(root, 'notes.md'), ELF_HEADER)
try {
await symlink(join(root, 'addon.node'), join(root, 'alias.node'))
} catch {
// Symlink creation can be restricted; the rest of the assertions still hold.
}
const found = collectNativeBinaries(root).map((p) => p.slice(root.length + 1))
expect(found).toContain('addon.node')
expect(found).toContain(join('nested', 'lib.so'))
expect(found).toContain(join('nested', 'lib.so.1'))
expect(found).toContain('orca-ide')
expect(found).not.toContain('script.js')
expect(found).not.toContain('text.node')
expect(found).not.toContain('notes.md')
expect(found).not.toContain('alias.node')
} finally {
await rm(root, { recursive: true, force: true })
}
})
})
describe.skipIf(process.platform === 'win32')('verifyLinuxGlibcFloor', () => {
// Stub objdump by basename to avoid random-temp collisions: -p needs, -T imports, fail exits nonzero, noutil omits libutil, pty imports openpty.
async function writeStubObjdump(dir) {
const stubPath = join(dir, 'objdump-stub.sh')
await writeFile(
stubPath,
[
'#!/bin/sh',
'if [ "$1" = "--version" ]; then echo "GNU objdump (stub)"; exit 0; fi',
'f=$(basename "$2")',
'case "$f" in',
' *fail*) echo "objdump: $f: File format not recognized" >&2; exit 1 ;;',
'esac',
'if [ "$1" = "-T" ]; then',
' case "$f" in',
' *pty*) printf "0000 DF *UND* 0000 (GLIBC_2.2.5) openpty\\n" ;;',
' esac',
' exit 0',
'fi',
'printf "Dynamic Section:\\n NEEDED libc.so.6\\n"',
'case "$f" in',
' *noutil*) : ;;',
' *) printf " NEEDED libutil.so.1\\n NEEDED libpthread.so.0\\n" ;;',
'esac',
'printf "\\nVersion References:\\n required from libc.so.6:\\n"',
'case "$f" in',
' *bad*) printf " 0x0 0x00 03 GLIBC_2.34\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
' *relr*) printf " 0x0 0x00 05 GLIBC_ABI_DT_RELR\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
' *weakonly*) printf " 0x0 0x02 06 GLIBC_2.32\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
' *cxx*|*sherpa*)',
' printf " required from libstdc++.so.6:\\n 0x0 0x00 07 GLIBCXX_3.4.29\\n" ;;',
' *) printf " 0x0 0x00 08 GLIBC_2.28\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
'esac',
'exit 0'
].join('\n'),
{ mode: 0o755 }
)
return stubPath
}
it('throws listing binaries over the floor (glibc, DT_RELR marker, and libstdc++)', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-over-'))
try {
const objdumpPath = await writeStubObjdump(root)
await mkdir(join(root, 'app', 'resources'), { recursive: true })
await writeFile(join(root, 'app', 'resources', 'bad-pty.node'), ELF_HEADER)
await writeFile(join(root, 'app', 'relr-exe.node'), ELF_HEADER)
await writeFile(join(root, 'app', 'cxx-addon.node'), ELF_HEADER) // launch-critical GLIBCXX_3.4.29
await writeFile(join(root, 'app', 'good.so'), ELF_HEADER)
let error
try {
verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })
} catch (e) {
error = e
}
expect(error).toBeDefined()
expect(error.message).toMatch(/bad-pty\.node needs GLIBC_2\.34/)
expect(error.message).toMatch(/relr-exe\.node needs GLIBC_ABI_DT_RELR/)
expect(error.message).toMatch(/cxx-addon\.node needs GLIBCXX_3\.4\.29/)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('throws when a pinned binary imports openpty without libutil.so.1 in DT_NEEDED', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-noutil-'))
try {
const objdumpPath = await writeStubObjdump(root)
await mkdir(join(root, 'app'), { recursive: true })
// Supported symbol version still fails without libutil.so.1 DT_NEEDED: openpty cannot resolve on Ubuntu20.04.
await writeFile(join(root, 'app', 'noutil-pty.node'), ELF_HEADER)
expect(() => verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })).toThrow(
/noutil-pty\.node imports openpty but libutil\.so\.1 is not in DT_NEEDED/
)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('passes weak/at-floor needs and the exempt sherpa-onnx libstdc++ prebuilt', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-under-'))
try {
const objdumpPath = await writeStubObjdump(root)
const sherpaDir = join(root, 'app', 'node_modules', 'sherpa-onnx-linux-x64')
await mkdir(sherpaDir, { recursive: true })
await writeFile(join(root, 'app', 'good-pty.node'), ELF_HEADER)
await writeFile(join(root, 'app', 'weakonly-lib.so'), ELF_HEADER) // weak GLIBC_2.32 → OK
await writeFile(join(root, 'app', 'orca-ide'), ELF_HEADER)
await writeFile(join(sherpaDir, 'sherpa-onnx.node'), ELF_HEADER) // GLIBCXX_3.4.29, exempt
expect(() => verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })).not.toThrow()
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('applies the floor profile it is given and names it in the failure', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-profile-'))
try {
const objdumpPath = await writeStubObjdump(root)
await mkdir(join(root, 'app'), { recursive: true })
await writeFile(join(root, 'app', 'good-pty.node'), ELF_HEADER) // GLIBC_2.28
const verify = (glibcFloor) =>
verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath, glibcFloor })
expect(() => verify(SERVER_SLOT_GLIBC_FLOOR)).not.toThrow()
expect(() => verify(COMPAT_SLOT_GLIBC_FLOOR)).toThrow(
/will not load on glibc 2\.17 \(CentOS 7[^\n]*\n\s+good-pty\.node needs GLIBC_2\.28/
)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('fails closed when objdump cannot read a binary (non-zero exit)', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-closed-'))
try {
const objdumpPath = await writeStubObjdump(root)
await mkdir(join(root, 'app'), { recursive: true })
await writeFile(join(root, 'app', 'unreadable-fail.node'), ELF_HEADER)
expect(() => verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })).toThrow(
/objdump -p failed/
)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('is a no-op (no objdump needed) when there are no native binaries', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-glibc-empty-'))
try {
await mkdir(join(root, 'app'), { recursive: true })
await writeFile(join(root, 'app', 'readme.txt'), 'no binaries here')
expect(() =>
verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath: '/nonexistent/objdump' })
).not.toThrow()
} finally {
await rm(root, { recursive: true, force: true })
}
})
})
/** Minimal little-endian 64-bit ELF header with the given e_machine. */
function elfHeader(machine) {
const header = Buffer.alloc(64)
header.write('\x7fELF', 0, 'latin1')
header[4] = 2 // ELFCLASS64
header[5] = 1 // ELFDATA2LSB
header[6] = 1 // EV_CURRENT
header.writeUInt16LE(3, 16) // ET_DYN
header.writeUInt16LE(machine, 18)
return header
}
describe('bundled native binary architecture', () => {
it('reads e_machine from a little-endian ELF', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const file = join(dir, 'pty.node')
await writeFile(file, elfHeader(0xb7))
expect(readElfMachine(file)).toBe(0xb7)
await rm(dir, { recursive: true, force: true })
})
// Catch wrong-arch source build despite valid symbol floors; retain intentionally vendored foreign-arch watcher judged by its own path.
it('accepts a per-arch vendored package that matches its own path', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const pkg = join(dir, '@parcel', 'watcher-linux-arm64-glibc')
await mkdir(pkg, { recursive: true })
const file = join(pkg, 'watcher.node')
await writeFile(file, elfHeader(0xb7))
expect(declaredArchFromPath(file)).toBe('arm64')
expect(findArchViolation(file, 'x64')).toBeNull()
await rm(dir, { recursive: true, force: true })
})
// But a path that names an arch must actually hold it — this is the Pi 5 failure.
it('flags a binary that contradicts the architecture its own path names', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const nested = join(dir, 'bin', 'linux-arm64-148')
await mkdir(nested, { recursive: true })
const file = join(nested, 'node-pty.node')
await writeFile(file, elfHeader(0x3e))
expect(findArchViolation(file, 'arm64')).toMatchObject({ actual: 'x64', expectedArch: 'arm64' })
// Still caught even when the slice being built is x64.
expect(findArchViolation(file, 'x64')).toMatchObject({ actual: 'x64', expectedArch: 'arm64' })
await rm(dir, { recursive: true, force: true })
})
// Release arm64 slices live in `linux-arm64-unpacked`; bundled ripgrep ships linux-x64 beside it.
it('reads arch tokens only below the slice root', async () => {
const root = join(await mkdtemp(join(tmpdir(), 'orca-elf-arch-')), 'linux-arm64-unpacked')
const dir = join(root, 'resources', 'ripgrep', 'linux-x64')
await mkdir(dir, { recursive: true })
const file = join(dir, 'rg')
await writeFile(file, elfHeader(0x3e))
expect(findArchViolation(file, 'arm64', root)).toBeNull()
await rm(dirname(root), { recursive: true, force: true })
})
it('flags an x86-64 binary in an arm64 slice', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const file = join(dir, 'pty.node')
await writeFile(file, elfHeader(0x3e))
expect(findArchViolation(file, 'arm64')).toMatchObject({ actual: 'x64' })
await rm(dir, { recursive: true, force: true })
})
it('accepts a matching architecture', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const file = join(dir, 'pty.node')
await writeFile(file, elfHeader(0x3e))
expect(findArchViolation(file, 'x64')).toBeNull()
await rm(dir, { recursive: true, force: true })
})
it('stays silent when no target architecture is supplied', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const file = join(dir, 'pty.node')
await writeFile(file, elfHeader(0x3e))
expect(findArchViolation(file, undefined)).toBeNull()
await rm(dir, { recursive: true, force: true })
})
it('ignores a file that is not a readable little-endian ELF', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const file = join(dir, 'not-elf.node')
await writeFile(file, Buffer.from('not an elf at all'))
expect(readElfMachine(file)).toBeNull()
expect(findArchViolation(file, 'arm64')).toBeNull()
await rm(dir, { recursive: true, force: true })
})
})