From 9b34114dd1cc5602bc8ff83421495e90936fc2a8 Mon Sep 17 00:00:00 2001 From: Kiyeon Jeon Date: Sun, 24 May 2026 15:46:09 +0900 Subject: [PATCH] fix(aws-lambda): fail build-zip when ffmpeg-static binary is not Linux x86-64 `ffmpeg-static` materializes a single platform-selected binary at install time. By default that follows the install host, so a default macOS/arm64 or linux/arm64 install can stage a binary that Lambda's x86-64 runtime cannot execute. Verify the ELF header (ELFCLASS64, EM_X86_64) before copying the binary into `bin/ffmpeg`. When the check fails, surface the canonical workarounds: run the build inside a linux/amd64 container, or pre-install with `npm_config_platform=linux npm_config_arch=x64`. In the distributed pipeline this mismatch can fail as early as Plan's `ffmpeg -version` probe after browser probing and before chunks are scheduled. Later encode/assemble spawns would fail for the same reason. ffprobe already goes through the platform-segmented `ffprobe-static/bin/linux/x64` path, so that side doesn't need the same check. --- packages/aws-lambda/scripts/build-zip.ts | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/aws-lambda/scripts/build-zip.ts b/packages/aws-lambda/scripts/build-zip.ts index 420524442..0586c3ca6 100644 --- a/packages/aws-lambda/scripts/build-zip.ts +++ b/packages/aws-lambda/scripts/build-zip.ts @@ -32,11 +32,14 @@ import { spawnSync } from "node:child_process"; import { chmodSync, + closeSync, cpSync, existsSync, mkdirSync, + openSync, readdirSync, readFileSync, + readSync, rmSync, statSync, writeFileSync, @@ -349,6 +352,55 @@ function stageHyperframeRuntime(stagingDir: string): void { console.log(`[build-zip] staged hyperframe.manifest.json + hyperframe.runtime.iife.js`); } +// ELF header constants used by `assertLinuxX86_64Elf`. Header layout: +// bytes 0..3 magic `0x7F 'E' 'L' 'F'`, byte 4 EI_CLASS (`2` = ELFCLASS64), +// bytes 18..19 e_machine little-endian (`0x3E` = EM_X86_64). +const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); +const ELF_CLASS_64 = 2; +const ELF_MACHINE_X86_64 = 0x3e; +const ELF_HEADER_BYTES = 20; + +/** + * Verify the binary at `path` is a Linux x86-64 ELF executable, the only + * shape the Lambda runtime can exec. Throws with the canonical workaround + * (Docker `--platform=linux/amd64` or `npm_config_platform` / + * `npm_config_arch` overrides) so the build doesn't ship a silently + * broken zip when a non-Linux host's postinstall fetched the wrong arch. + */ +function assertLinuxX86_64Elf(path: string, label: string): void { + const head = readFileHead(path, ELF_HEADER_BYTES, label); + const isElf = head.subarray(0, 4).equals(ELF_MAGIC); + const isElf64 = head[4] === ELF_CLASS_64; + const machine = head.readUInt16LE(18); + if (isElf && isElf64 && machine === ELF_MACHINE_X86_64) return; + + const magicHex = head.subarray(0, 4).toString("hex"); + throw new Error( + `[build-zip] ${label} at ${path} is not a Linux x86-64 ELF executable ` + + `(magic=0x${magicHex}, ei_class=${head[4]}, e_machine=0x${machine.toString(16)}). ` + + `This usually means the deploy host's postinstall fetched a host-platform binary ` + + `(e.g. macOS arm64 ffmpeg) instead of the linux/x64 binary Lambda needs. ` + + `Re-run the build inside a linux/amd64 container, or pre-install with ` + + `\`npm_config_platform=linux npm_config_arch=x64\` so the package fetches the right binary.`, + ); +} + +function readFileHead(path: string, byteCount: number, label: string): Buffer { + const fd = openSync(path, "r"); + try { + const buf = Buffer.alloc(byteCount); + const bytesRead = readSync(fd, buf, 0, byteCount, 0); + if (bytesRead < byteCount) { + throw new Error( + `[build-zip] ${label} at ${path} is too short — read ${bytesRead} of ${byteCount} bytes.`, + ); + } + return buf; + } finally { + closeSync(fd); + } +} + function stageFfmpeg(stagingDir: string): void { const binDir = join(stagingDir, "bin"); mkdirSync(binDir, { recursive: true }); @@ -356,12 +408,19 @@ function stageFfmpeg(stagingDir: string): void { // ffmpeg from `ffmpeg-static`. The package only ships the encoder // binary; the audio pad/trim path also needs ffprobe, which comes // from `ffprobe-static`. + // + // `ffmpeg-static`'s postinstall fetches a binary for the host platform + // (e.g. arm64 Mach-O on Apple Silicon macOS). Lambda runs Linux x86-64, + // so a build from a non-Linux host silently produces a zip that boots + // but fails at first ffmpeg invocation with `cannot execute binary file`. + // Verify the ELF header up front and bail with a clear message instead. const ffmpegBinary = join(resolveModuleDir("ffmpeg-static"), "ffmpeg"); if (!existsSync(ffmpegBinary)) { throw new Error( `[build-zip] ffmpeg-static binary missing at ${ffmpegBinary}. Did postinstall run?`, ); } + assertLinuxX86_64Elf(ffmpegBinary, "ffmpeg-static binary"); const ffmpegDest = join(binDir, "ffmpeg"); cpSync(ffmpegBinary, ffmpegDest); chmodSync(ffmpegDest, 0o755);