Files
pnpm/.github/scripts/assert-windows-crt-static.test.mjs
3172a576b7 fix: statically link CRT for windows-msvc pnpm binaries (#15853)
The published Windows pnpm.exe linked VCRUNTIME140.dll, so it exited
0xC0000135 on a clean Windows install. The Rust release job now passes
-C target-feature=+crt-static when building the CLI for
*-pc-windows-msvc. The NAPI cdylib build is unchanged, including the
musl -crt-static flag.

The release verification job rejects a Windows pnpm.exe that still
imports that DLL.

Closes https://github.com/pnpm/pnpm/issues/15723

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:16:54 +02:00

136 lines
5.6 KiB
JavaScript

import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { dynamicVcRuntimeDlls, importedDlls } from './assert-windows-crt-static.mjs'
const script = fileURLToPath(new URL('./assert-windows-crt-static.mjs', import.meta.url))
test('a Windows binary may import the universal CRT and kernel32', () => {
const dlls = ['KERNEL32.dll', 'api-ms-win-crt-runtime-l1-1-0.dll', 'ucrtbase.dll']
const bytes = peWithImports(dlls)
assert.deepEqual(importedDlls(bytes), dlls)
assert.deepEqual(dynamicVcRuntimeDlls(importedDlls(bytes)), [])
assert.equal(run(bytes), 0)
})
test('VCRUNTIME140.dll is rejected, including the versioned and uppercase names', () => {
for (const name of ['VCRUNTIME140.dll', 'VCRUNTIME140_1.dll', 'vcruntime140D.dll']) {
const bytes = peWithImports(['KERNEL32.dll', name], { pe32Plus: name !== 'vcruntime140D.dll' })
assert.deepEqual(dynamicVcRuntimeDlls(importedDlls(bytes)), [name])
assert.equal(run(bytes), 1)
}
})
test('a file that is not a PE fails the check', () => {
assert.throws(() => importedDlls(Buffer.from('not a pe')), /not a PE file/)
assert.equal(run(Buffer.from('MZ')), 1)
})
test('an import directory in a section\'s zero-filled tail fails the check', () => {
// Trailing bytes after the section's raw data would otherwise read as an
// empty import directory.
const bytes = Buffer.concat([peWithImports(['VCRUNTIME140.dll']), Buffer.alloc(0x400)])
const sectionHeader = 0x80 + 24 + 240
const rawSize = bytes.readUInt32LE(sectionHeader + 16)
bytes.writeUInt32LE(rawSize * 2, sectionHeader + 8)
bytes.writeUInt32LE(0x1000 + rawSize, 0x80 + 24 + 112 + 8)
assert.throws(() => importedDlls(bytes), /points outside the file/)
assert.equal(run(bytes), 1)
})
test('a DLL name that runs past the section\'s raw data fails the check', () => {
const pe = peWithImports(['VCRUNTIME140.dll'])
const rawStart = 0x200
const nameStart = rawStart + 2 * 20
pe.fill('A', nameStart)
const bytes = Buffer.concat([pe, Buffer.alloc(16)])
assert.throws(() => importedDlls(bytes), /unterminated import DLL name/)
assert.equal(run(bytes), 1)
})
test('import names match objdump', () => {
const dlls = ['KERNEL32.dll', 'VCRUNTIME140.dll']
const bytes = peWithImports(dlls)
const dir = mkdtempSync(path.join(tmpdir(), 'pnpm-crt-'))
const file = path.join(dir, 'sample.exe')
writeFileSync(file, bytes)
const dump = execFileSync('objdump', ['-p', file], { encoding: 'utf8' })
const fromDump = [...dump.matchAll(/DLL Name:\s*(\S+)/g)].map(match => match[1])
assert.deepEqual(importedDlls(readFileSync(file)), fromDump)
assert.deepEqual(fromDump, dlls)
})
function run (bytes) {
const dir = mkdtempSync(path.join(tmpdir(), 'pnpm-crt-'))
const file = path.join(dir, 'sample.exe')
writeFileSync(file, bytes)
try {
execFileSync(process.execPath, [script, file], { stdio: 'pipe' })
return 0
} catch (error) {
return error.status ?? 1
}
}
// Minimal PE whose import directory objdump can read. Only the fields the
// loader and objdump need for DLL names are filled in.
function peWithImports (dllNames, { pe32Plus = true } = {}) {
const fileAlignment = 0x200
const sectionAlignment = 0x1000
const peOffset = 0x80
const optionalSize = pe32Plus ? 240 : 224
const headersSize = peOffset + 24 + optionalSize + 40
const alignedHeaders = Math.ceil(headersSize / fileAlignment) * fileAlignment
const descriptorsSize = (dllNames.length + 1) * 20
const nameBlobs = dllNames.map(name => Buffer.from(`${name}\0`, 'ascii'))
let nameCursor = descriptorsSize
const nameOffsets = nameBlobs.map(blob => {
const offset = nameCursor
nameCursor += blob.length
return offset
})
const section = Buffer.alloc(nameCursor)
for (const [index, blob] of nameBlobs.entries()) {
section.writeUInt32LE(sectionAlignment + nameOffsets[index], index * 20 + 12)
section.writeUInt32LE(1, index * 20 + 16)
blob.copy(section, nameOffsets[index])
}
const rawSectionSize = Math.ceil(section.length / fileAlignment) * fileAlignment
const file = Buffer.alloc(alignedHeaders + rawSectionSize)
file.write('MZ', 0, 'ascii')
file.writeUInt32LE(peOffset, 0x3c)
file.write('PE\0\0', peOffset, 'ascii')
const coff = peOffset + 4
file.writeUInt16LE(pe32Plus ? 0x8664 : 0x14c, coff)
file.writeUInt16LE(1, coff + 2)
file.writeUInt16LE(optionalSize, coff + 16)
file.writeUInt16LE(0x22, coff + 18)
const optional = coff + 20
file.writeUInt16LE(pe32Plus ? 0x20b : 0x10b, optional)
file.writeUInt32LE(sectionAlignment, optional + 32)
file.writeUInt32LE(fileAlignment, optional + 36)
file.writeUInt16LE(6, optional + 48)
const sizeOfImage = sectionAlignment + Math.ceil(section.length / sectionAlignment) * sectionAlignment
file.writeUInt32LE(sizeOfImage, optional + 56)
file.writeUInt32LE(alignedHeaders, optional + 60)
const dataDirectory = optional + (pe32Plus ? 112 : 96)
file.writeUInt32LE(16, dataDirectory - 4)
file.writeUInt32LE(sectionAlignment, dataDirectory + 8)
file.writeUInt32LE(descriptorsSize, dataDirectory + 12)
const sectionHeader = optional + optionalSize
file.write('.idata', sectionHeader, 'ascii')
file.writeUInt32LE(section.length, sectionHeader + 8)
file.writeUInt32LE(sectionAlignment, sectionHeader + 12)
file.writeUInt32LE(rawSectionSize, sectionHeader + 16)
file.writeUInt32LE(alignedHeaders, sectionHeader + 20)
section.copy(file, alignedHeaders)
return file
}