From 0688279184d2ade7d649ef479eabaabdd43003ce Mon Sep 17 00:00:00 2001 From: Deepak Mohan Date: Mon, 1 Jun 2026 23:54:53 +0900 Subject: [PATCH] build: clamp PT_LOAD p_align to 4 KiB on linux-x64 node binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node.js 23/24 linux-x64 build ships a dedicated `lpstub` PT_LOAD segment aligned to 2 MiB (0x200000) so that, at startup the binary can remap its .text region onto Linux hugepages for an iTLB win `--use-largepages=mode`. WSL1 `binfmt_elf` strictly rejects any PT_LOAD whose `p_align` exceeds the system page size (0x1000); the kernel returns ENOEXEC and the shell reports "exec format error" before user space ever runs. This breaks launching the bundled Node from the VS Code server under WSL1 starting with Node 24. The fix does not change runtime behavior: - Node is a non-PIE EXEC binary with fixed virtual addresses (`p_vaddr`, `p_paddr`); the loader maps each PT_LOAD at its hard-coded address regardless of `p_align`, which on EXEC is metadata describing alignment in memory rather than a request to relocate. - The hugepage optimization itself is performed at runtime against the live mapping; it does not consult `p_align` and is unaffected by this change. - No shared libraries, native addons, or dynamic linker paths are touched — only PT_LOAD segments inside the node executable itself. --- build/gulpfile.reh.ts | 48 +++++++++++++++++++++++++++++++++++-- test/sanity/src/context.ts | 33 ------------------------- test/sanity/src/wsl.test.ts | 2 -- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts index 7208837f4acb1..beb7ba848fd31 100644 --- a/build/gulpfile.reh.ts +++ b/build/gulpfile.reh.ts @@ -163,6 +163,48 @@ function extractAlpinefromDocker(nodeVersion: string, platform: string, arch: st return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } as fs.Stats })]); } +// WSL1 binfmt_elf rejects PT_LOAD segments with p_align > PAGE_SIZE (0x1000). +// Node 24 linux-x64 ships an `lpstub` LOAD segment aligned to 2 MiB for hugepage +// remapping; clamp it so the binary still loads under WSL1. +function patchElfLoadAlign(): NodeJS.ReadWriteStream { + return es.mapSync(file => { + if (!file.contents || !Buffer.isBuffer(file.contents)) { + return file; + } + const buf = file.contents; + if (buf.length < 64) { + return file; + } + if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46) { + return file; + } + if (buf[4] !== 2 /* ELFCLASS64 */ || buf[5] !== 1 /* ELFDATA2LSB */) { + return file; + } + const e_phoff = Number(buf.readBigUInt64LE(0x20)); + const e_phentsize = buf.readUInt16LE(0x36); + const e_phnum = buf.readUInt16LE(0x38); + if (e_phentsize !== 56) { + return file; + } + const PT_LOAD = 1; + const MAX_ALIGN = 0x1000n; + for (let i = 0; i < e_phnum; i++) { + const off = e_phoff + i * e_phentsize; + if (off + e_phentsize > buf.length) { + break; + } + if (buf.readUInt32LE(off) !== PT_LOAD) { + continue; + } + if (buf.readBigUInt64LE(off + 48) > MAX_ALIGN) { + buf.writeBigUInt64LE(MAX_ALIGN, off + 48); + } + } + return file; + }); +} + const { nodeVersion, internalNodeVersion } = getNodeVersion(); BUILD_TARGETS.forEach(({ platform, arch }) => { @@ -229,14 +271,16 @@ function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefi fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) .pipe(rename('node.exe')); case 'darwin': - case 'linux': - return (product.nodejsRepository !== 'https://nodejs.org' ? + case 'linux': { + const downloaded = (product.nodejsRepository !== 'https://nodejs.org' ? fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) .pipe(util.setExecutableBit('**')) .pipe(rename('node')); + return platform === 'linux' && arch === 'x64' ? downloaded.pipe(patchElfLoadAlign()) : downloaded; + } case 'alpine': return product.nodejsRepository !== 'https://nodejs.org' ? fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts index 990252bc48bd4..3fc08f3c3796a 100644 --- a/test/sanity/src/context.ts +++ b/test/sanity/src/context.ts @@ -42,7 +42,6 @@ export class TestContext { private readonly tempDirs = new Set(); private readonly wslTempDirs = new Set(); - private readonly patchedWslNodePaths = new Set(); private nextPort = 3010; private currentTestName: string | undefined; private screenshotCounter = 0; @@ -257,38 +256,6 @@ export class TestContext { return undefined; } - /** - * On WSL1, patches the Node.js binary used by the server to remove ELF note sections - * that cause Node 24 to fail to start. No-op on WSL2. - * @param wslEntryPoint The WSL path to the server entry point script. - */ - public applyWsl1Node24Workaround(wslEntryPoint: string): void { - if (this.getUbuntuWslVersion() !== 1) { - return; - } - - const wslNodePath = wslEntryPoint.replace(/\/bin\/[^/]+$/, '/node'); - if (this.patchedWslNodePaths.has(wslNodePath)) { - return; - } - - this.patchedWslNodePaths.add(wslNodePath); - this.warn(`Applying WSL1 Node 24 workaround for ${wslNodePath}`); - - const shellScript = [ - 'set -e', - `node_path='${wslNodePath}'`, - 'backup_path="${node_path}.orig"', - 'if [ -f "${backup_path}" ]; then exit 0; fi', - 'if ! command -v objcopy >/dev/null 2>&1; then apt-get update && apt-get install -y binutils; fi', - 'cp "${node_path}" "${backup_path}"', - 'objcopy --remove-section .note.ABI-tag --remove-section .note.gnu.build-id --remove-section .note.gnu.property "${backup_path}" "${node_path}"', - 'chmod +x "${node_path}"', - ].join('; '); - - this.runNoErrors('wsl', '-d', 'Ubuntu', 'sh', '-lc', shellScript); - } - /** * Ensures that the directory for the specified file path exists. */ diff --git a/test/sanity/src/wsl.test.ts b/test/sanity/src/wsl.test.ts index 0e6b56d23b8b9..65a6455fee2d8 100644 --- a/test/sanity/src/wsl.test.ts +++ b/test/sanity/src/wsl.test.ts @@ -61,7 +61,6 @@ export function setup(context: TestContext) { } const wslEntryPoint = context.toWslPath(entryPoint); - context.applyWsl1Node24Workaround(wslEntryPoint); await context.runCliApp('WSL Server', 'wsl', [ @@ -102,7 +101,6 @@ export function setup(context: TestContext) { const test = new WslUITest(context, undefined, wslWorkspaceDir, wslExtensionsDir); const wslEntryPoint = context.toWslPath(entryPoint); - context.applyWsl1Node24Workaround(wslEntryPoint); await context.runCliApp('WSL Server', 'wsl', [