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>
136 lines
5.6 KiB
JavaScript
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
|
|
}
|