fix(cli): surface HYPERFRAMES_BROWSER_PATH hint on macOS <13 chrome-headless-shell dyld crash

Field feedback (#hyperframes-cli-feedback ts=1784227832, darwin/x64,
macOS 12, HyperFrames CLI 0.7.60) hit
`dyld: Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount`
from VideoToolbox when launching the pinned chrome-headless-shell
mac-152.0.7928.2. The symbol is macOS-13-only, so older hosts abort
the binary at dyld load before any browser process starts.

The reporter recovered by installing an older shell
(`@puppeteer/browsers install chrome-headless-shell@150`) and pointing
`PRODUCER_HEADLESS_SHELL_PATH` at it. Their check/snapshot commands
accepted that older cached shell (they do not force the pinned build),
but the render command requires v152 via `preferManagedChrome: true`
and could not fall back on its own. The generic "Try --docker" hint
didn't name any of the browser-path env vars.

Sibling failure mode to the download-time hint added in #2443 and the
closed-with-invite #2078 (SIGTRAP at launch on macOS arm64), and the
in-flight #2481 (Windows STATUS_STACK_BUFFER_OVERRUN); same
`HYPERFRAMES_BROWSER_PATH` remediation, different trigger + platform.

The match is gated on:
1. Puppeteer launch-failure wrapper text
2. dyld Symbol-not-found signal
3. a macOS-13-only symbol OR the VideoToolbox framework

so unrelated darwin launch failures do not mis-fire the hint. The
symbol name is macOS-version-specific by construction — if a user's
dyld cannot find `_kVTCompressionPropertyKey_ReferenceBufferCount`
their host is <13, no separate `os.release()` gate needed.

- Signed-off-by: Via -
This commit is contained in:
Via
2026-07-16 19:25:45 +00:00
parent 4b9135393e
commit 0d16f19b07
3 changed files with 182 additions and 0 deletions
@@ -0,0 +1,100 @@
import { describe, it, expect } from "vitest";
import {
isMacosOldChromeCrashError,
macosOldChromeCrashRemediation,
} from "./macosOldChromeCrash.js";
describe("isMacosOldChromeCrashError", () => {
it("matches Puppeteer launch-failure wrapper + dyld Symbol-not-found + macOS-13 VideoToolbox symbol", () => {
expect(
isMacosOldChromeCrashError(
"Failed to launch the browser process!\n" +
"dyld: Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount\n" +
" Referenced from: /Users/x/.cache/puppeteer/chrome-headless-shell/mac-152.0.7928.2/chrome-headless-shell\n" +
" Expected in: /System/Library/Frameworks/VideoToolbox.framework/Versions/A/VideoToolbox",
),
).toBe(true);
});
it("matches the VideoToolbox framework signal even without the exact ReferenceBufferCount symbol name", () => {
// Future pinned builds may hit a sibling VT symbol from the same framework
// (they all migrated to macOS 13); the framework signal is enough to fire
// the same remediation.
expect(
isMacosOldChromeCrashError(
"Failed to launch the browser process. Symbol not found: _kVTSomeFutureSymbol from VideoToolbox",
),
).toBe(true);
});
it("does not match a launch failure without dyld Symbol-not-found", () => {
expect(
isMacosOldChromeCrashError(
"Failed to launch the browser process (libnss3.so: cannot open shared object file)",
),
).toBe(false);
});
it("does not match a darwin Symbol-not-found from an unrelated framework (needs different remediation)", () => {
expect(
isMacosOldChromeCrashError(
"Failed to launch the browser process. dyld: Symbol not found: _some_libcxx_symbol from libc++.dylib",
),
).toBe(false);
});
it("does not match the symbol name alone without the launch-failure wrapper", () => {
expect(
isMacosOldChromeCrashError(
"unrelated tool crashed with Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount",
),
).toBe(false);
});
it("does not match unrelated errors", () => {
expect(isMacosOldChromeCrashError("Composition HTML is empty")).toBe(false);
});
});
describe("macosOldChromeCrashRemediation", () => {
it("returns undefined off darwin even for a matching error", () => {
if (process.platform === "darwin") return;
expect(
macosOldChromeCrashRemediation(
"Failed to launch the browser process. dyld: Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount from VideoToolbox",
),
).toBeUndefined();
});
it("returns undefined for non-launch errors on any platform", () => {
expect(macosOldChromeCrashRemediation("Composition HTML is empty")).toBeUndefined();
});
it("returns undefined for a launch failure without the dyld signal on any platform", () => {
expect(
macosOldChromeCrashRemediation(
"Failed to launch the browser process (libnss3.so cannot open)",
),
).toBeUndefined();
});
it("returns undefined for a darwin dyld failure from an unrelated framework", () => {
if (process.platform !== "darwin") return;
expect(
macosOldChromeCrashRemediation(
"Failed to launch the browser process. Symbol not found: _some_libcxx_symbol from libc++.dylib",
),
).toBeUndefined();
});
it("mentions HYPERFRAMES_BROWSER_PATH and the PRODUCER_HEADLESS_SHELL_PATH alias when firing on darwin", () => {
if (process.platform !== "darwin") return;
const remediation = macosOldChromeCrashRemediation(
"Failed to launch the browser process. dyld: Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount from VideoToolbox",
);
expect(remediation).toBeDefined();
expect(remediation).toMatch(/HYPERFRAMES_BROWSER_PATH/);
expect(remediation).toMatch(/PRODUCER_HEADLESS_SHELL_PATH/);
expect(remediation).toMatch(/chrome-headless-shell@150/);
});
});
@@ -0,0 +1,73 @@
// fallow-ignore-file code-duplication
/**
* Detection + remediation for macOS-below-13 chrome-headless-shell dyld
* launch crashes.
*
* Field feedback (#hyperframes-cli-feedback ts=1784227832, darwin/x64,
* macOS 12, HyperFrames CLI 0.7.60) hit
* `dyld: Symbol not found: _kVTCompressionPropertyKey_ReferenceBufferCount`
* from `VideoToolbox` when launching the pinned
* `chrome-headless-shell mac-152.0.7928.2`. The symbol was added in
* macOS 13; older hosts cannot resolve it, so the binary aborts at load
* before any browser process starts.
*
* The reporter recovered by installing an older shell (v150) via
* `@puppeteer/browsers install chrome-headless-shell@150` and pointing
* `PRODUCER_HEADLESS_SHELL_PATH` at it. Their check/snapshot commands
* accepted that older cached shell (they don't force the pinned build),
* but the render command required v152 via `preferManagedChrome: true`
* and therefore could not fall back on its own. The generic
* "Try --docker for containerized rendering" hint didn't name any of
* the browser-path env vars, so the workaround is undiscoverable
* unaided.
*
* Same discoverability class as #2443 (download failure), #2078 (arm64
* SIGTRAP at launch), and #2481 (Windows STATUS_STACK_BUFFER_OVERRUN):
* detect the launch-time crash signal, surface `HYPERFRAMES_BROWSER_PATH`
* (and its `PRODUCER_HEADLESS_SHELL_PATH` alias — see #2471) with a
* concrete macOS example.
*
* The match is gated on the Puppeteer launch-failure wrapper text AND
* the dyld symbol-not-found signal AND a macOS-13-only symbol, so
* unrelated darwin launch failures do not mis-fire this hint. The
* symbol-name signal is macOS-version-specific by construction:
* `_kVTCompressionPropertyKey_ReferenceBufferCount` only exists on
* macOS 13+, so if a user's dyld cannot find it, their host is <13.
* That means we do not need a separate `os.release()` gate — the
* signal itself is the version discriminator.
*/
const DYLD_SYMBOL_NOT_FOUND = /Symbol not found:/i;
const MACOS_13_ONLY_SYMBOL = /_kVTCompressionPropertyKey_ReferenceBufferCount/;
const VIDEO_TOOLBOX = /VideoToolbox/;
export function isMacosOldChromeCrashError(errorMessage: string): boolean {
if (!/Failed to launch the browser process/i.test(errorMessage)) return false;
if (!DYLD_SYMBOL_NOT_FOUND.test(errorMessage)) return false;
// Both signals must be present. The specific macOS-13-only symbol is what
// makes this hint safe to fire (any other Symbol-not-found on darwin
// needs different remediation — a shared-lib install, an Xcode SDK
// mismatch, etc. — none of which are fixed by HYPERFRAMES_BROWSER_PATH).
// We also require VideoToolbox to catch the case where a future pinned
// build hits a different `_kVT…` symbol from the same framework.
return MACOS_13_ONLY_SYMBOL.test(errorMessage) || VIDEO_TOOLBOX.test(errorMessage);
}
export function macosOldChromeCrashRemediation(errorMessage: string): string | undefined {
if (process.platform !== "darwin") return undefined;
if (!isMacosOldChromeCrashError(errorMessage)) return undefined;
return [
"chrome-headless-shell crashed at launch (macOS dyld: Symbol not found in VideoToolbox).",
"The pinned Chromium build requires macOS 13+; on macOS 12 or older, install an older",
"chrome-headless-shell and point hyperframes at it:",
"",
" npx @puppeteer/browsers install chrome-headless-shell@150",
' export HYPERFRAMES_BROWSER_PATH="$HOME/.cache/puppeteer/chrome-headless-shell/mac-150.0.7422.0/chrome-headless-shell-mac-x64/chrome-headless-shell"',
"",
"(PRODUCER_HEADLESS_SHELL_PATH works as an alias for the same override.)",
"Any working chrome-headless-shell build resolves this — the exact version above is one",
"known-good macOS-12 combination. Alternatively, point HYPERFRAMES_BROWSER_PATH at your",
'installed Google Chrome ("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")',
"to fall back to the screenshot capture path.",
].join("\n");
}
+9
View File
@@ -80,6 +80,7 @@ import { formatRenderOutputTimestamp } from "@hyperframes/core";
import { runEnvironmentChecks } from "../browser/preflight.js";
import { detectH264EncoderMode } from "../browser/ffmpeg.js";
import { chromeLaunchRemediation } from "../browser/linuxDeps.js";
import { macosOldChromeCrashRemediation } from "../browser/macosOldChromeCrash.js";
import { killOrphanedProcesses } from "../utils/orphanCleanup.js";
import {
markRenderSucceeded,
@@ -2106,6 +2107,14 @@ function handleRenderError(
errorBox("Render failed — Chrome could not launch", message, remediation);
process.exit(1);
}
// macOS <13 dyld Symbol-not-found on the pinned chrome-headless-shell
// build. Different remediation shape (older shell + env-var override)
// than the Linux shared-lib install, so it lives in its own detector.
const macosRemediation = macosOldChromeCrashRemediation(message);
if (macosRemediation) {
errorBox("Render failed — Chrome could not launch", message, macosRemediation);
process.exit(1);
}
errorBox("Render failed", message, hint);
process.exit(1);
}