diff --git a/.eslint/rules/file-suffix.js b/.eslint/rules/file-suffix.js index 423ae90870..4c18df824d 100644 --- a/.eslint/rules/file-suffix.js +++ b/.eslint/rules/file-suffix.js @@ -108,6 +108,7 @@ const NODE_PACKAGES = new Set([ 'node-gyp-build', 'npm-run-all', 'p-limit', + 'pe-library', 'pixelmatch', 'playwright', 'postcss', @@ -116,7 +117,6 @@ const NODE_PACKAGES = new Set([ 'prettier-plugin-tailwindcss', 'react-devtools', 'react-devtools-core', - 'resedit', 'resolve-url-loader', 'sass', 'sass-loader', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7072479561..0aeab8c610 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -463,7 +463,7 @@ jobs: continue-on-error: true strategy: matrix: - os: [ubuntu-22.04-8-cores, macos-latest] + os: [ubuntu-22.04-8-cores, macos-latest, windows-latest-8-cores] runs-on: ${{ matrix.os }} timeout-minutes: 30 diff --git a/package.json b/package.json index 82c22c0800..824a51b3d9 100644 --- a/package.json +++ b/package.json @@ -333,6 +333,7 @@ "node-gyp-build": "4.8.4", "npm-run-all": "4.1.5", "p-limit": "3.1.0", + "pe-library": "2.0.1", "pixelmatch": "5.3.0", "playwright": "1.58.2", "pngjs": "7.0.0", @@ -342,7 +343,6 @@ "prettier-plugin-tailwindcss": "0.7.2", "react-devtools": "6.0.1", "react-devtools-core": "6.0.1", - "resedit": "2.0.2", "resolve-url-loader": "5.0.0", "sass": "1.80.7", "sass-loader": "16.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3de4fb0abd..ca786cb66a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -729,6 +729,9 @@ importers: p-limit: specifier: 3.1.0 version: 3.1.0 + pe-library: + specifier: 2.0.1 + version: 2.0.1 pixelmatch: specifier: 5.3.0 version: 5.3.0 @@ -756,9 +759,6 @@ importers: react-devtools-core: specifier: 6.0.1 version: 6.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - resedit: - specifier: 2.0.2 - version: 2.0.2 resolve-url-loader: specifier: 5.0.0 version: 5.0.0 @@ -8670,9 +8670,9 @@ packages: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} - pe-library@1.0.1: - resolution: {integrity: sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==} - engines: {node: '>=14', npm: '>=7'} + pe-library@2.0.1: + resolution: {integrity: sha512-/qjYFqNSlq59B5DI36am++5/3gMgh02QnzpYigrwrW6s+QpU0mHf09/iA4wjTu21UUxodyV7ZCetV5MiDhaN/A==} + engines: {node: '>=20'} pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -9359,10 +9359,6 @@ packages: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} - resedit@2.0.2: - resolution: {integrity: sha512-UKTnq602iVe+W5SyRAQx/WdWMnlDiONfXBLFg/ur4QE4EQQ8eP7Jgm5mNXdK12kKawk1vvXPja2iXKqZiGDW6Q==} - engines: {node: '>=14', npm: '>=7'} - reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -20417,7 +20413,7 @@ snapshots: pe-library@0.4.1: {} - pe-library@1.0.1: {} + pe-library@2.0.1: {} pend@1.2.0: {} @@ -21231,10 +21227,6 @@ snapshots: dependencies: pe-library: 0.4.1 - resedit@2.0.2: - dependencies: - pe-library: 1.0.1 - reselect@5.1.1: {} resolve-alpn@1.2.1: {} diff --git a/ts/scripts/check-min-os-version.node.ts b/ts/scripts/check-min-os-version.node.ts index b9ba96807b..6507908025 100644 --- a/ts/scripts/check-min-os-version.node.ts +++ b/ts/scripts/check-min-os-version.node.ts @@ -4,9 +4,11 @@ import { execFile as execFileCb } from 'node:child_process'; import { promisify } from 'node:util'; import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; import { join, basename } from 'node:path'; import fastGlob from 'fast-glob'; import { gte } from 'semver'; +import { Format, NtExecutable } from 'pe-library'; // Note: because we don't run under electron - this is a path to binary import ELECTRON_BINARY from 'electron'; @@ -14,6 +16,8 @@ import ELECTRON_BINARY from 'electron'; import { drop } from '../util/drop.std.js'; import packageJson from '../util/packageJson.node.js'; +const { ImageDosHeader, ImageNtHeaders, ImageDirectoryEntry } = Format; + const execFile = promisify(execFileCb); // See https://en.wikipedia.org/wiki/Darwin_(operating_system)#Darwin_20_onwards @@ -86,6 +90,164 @@ async function macosVersionCheck(file: string) { ); } +// See: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format?redirectedfrom=MSDN +// See: https://0xrick.github.io/win-internals/pe6/ +const EMPTY_IMPORT_ENTRY = Buffer.alloc(4 * 5); +const EMPTY_DELAY_IMPORT_ENTRY = Buffer.alloc(4 * 8); + +const DLL_TABLES = new Map([ + // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table + [ImageDirectoryEntry.Import, { empty: EMPTY_IMPORT_ENTRY, nameOffset: 12 }], + // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-delay-load-directory-table + [ + ImageDirectoryEntry.DelayImport, + { empty: EMPTY_DELAY_IMPORT_ENTRY, nameOffset: 4 }, + ], +]); + +const ALLOWED_DLLS = new Set([ + 'advapi32.dll', + 'api-ms-win-core-handle-l1-1-0.dll', + 'api-ms-win-core-realtime-l1-1-1.dll', + 'api-ms-win-core-synch-l1-2-0.dll', + 'api-ms-win-core-winrt-error-l1-1-1.dll', + 'api-ms-win-core-winrt-l1-1-0.dll', + 'api-ms-win-core-winrt-string-l1-1-0.dll', + 'api-ms-win-power-base-l1-1-0.dll', + 'api-ms-win-shcore-scaling-l1-1-1.dll', + 'avrt.dll', + 'bcrypt.dll', + 'bcryptprimitives.dll', + 'bthprops.cpl', + 'cfgmgr32.dll', + 'comctl32.dll', + 'comdlg32.dll', + 'crypt32.dll', + 'd3d11.dll', + 'd3d12.dll', + 'dbghelp.dll', + 'dcomp.dll', + 'dhcpcsvc.dll', + 'dwmapi.dll', + 'dwrite.dll', + 'dxgi.dll', + 'ffmpeg.dll', + 'fontsub.dll', + 'gdi32.dll', + 'hid.dll', + 'iphlpapi.dll', + 'kernel32.dll', + 'mf.dll', + 'mfplat.dll', + 'mfreadwrite.dll', + 'mmdevapi.dll', + 'msdmo.dll', + 'ncrypt.dll', + 'node.exe', + 'ntdll.dll', + 'ole32.dll', + 'oleacc.dll', + 'oleaut32.dll', + 'pdh.dll', + 'powrprof.dll', + 'propsys.dll', + 'psapi.dll', + 'rpcrt4.dll', + 'secur32.dll', + 'setupapi.dll', + 'shell32.dll', + 'shlwapi.dll', + 'uiautomationcore.dll', + 'urlmon.dll', + 'user32.dll', + 'userenv.dll', + 'uxtheme.dll', + 'version.dll', + 'winhttp.dll', + 'winmm.dll', + 'winspool.drv', + 'wintrust.dll', + 'winusb.dll', + 'ws2_32.dll', + 'wtsapi32.dll', +]); + +async function windowsDllImportCheck(file: string): Promise { + console.log(`${file}: checking...`); + + const fileData = await readFile(file); + const dosHeader = ImageDosHeader.from(fileData); + const ntHeaders = ImageNtHeaders.from(fileData, dosHeader.newHeaderAddress); + + const ntExecutable = NtExecutable.from(fileData, { + ignoreCert: true, + }); + + function cstr(data: Buffer, offset: number): string { + for (let end = offset; end < data.length; end += 1) { + if (data[end] === 0) { + return data.subarray(offset, end).toString(); + } + } + throw new Error('Invalid cstring'); + } + + const imports = new Set(); + for (const [entryType, { empty, nameOffset }] of DLL_TABLES) { + const section = ntExecutable.getSectionByEntry(entryType); + const imageDirectoryEntry = + ntHeaders.optionalHeaderDataDirectory.get(entryType); + + if (section?.data == null || imageDirectoryEntry == null) { + console.warn(`${file}: no ${entryType} directory entry`); + continue; + } + + // section contains the directory entry, but at offset determined by the + // image directory entry + const entryData = Buffer.from(section.data).subarray( + imageDirectoryEntry.virtualAddress - section.info.virtualAddress + ); + + for (let i = 0; i < entryData.byteLength; i += empty.byteLength) { + const entry = entryData.subarray(i, i + empty.byteLength); + + // Empty descriptor indicates end of the array + if (entry.equals(empty)) { + break; + } + + const name = entry.readInt32LE(nameOffset); + + if (name <= 0) { + continue; + } + imports.add( + cstr( + fileData, + // `name` is offest relative to loaded section, translate it back + // to the file offset + name - section.info.virtualAddress + section.info.pointerToRawData + ).toLowerCase() + ); + } + } + + let disallowed = 0; + for (const name of imports) { + if (ALLOWED_DLLS.has(name)) { + console.log(` Allowed: ${name}`); + } else { + console.error(` Disallowed: ${name}`); + disallowed += 1; + } + } + + if (disallowed !== 0) { + throw new Error(`${basename(file)} contains disallowed dll imports`); + } +} + function padGlibcVersion(version: string) { if (/^\d+\.\d+$/.test(version)) { return `${version}.0`; @@ -166,13 +328,14 @@ async function main() { } )), ]; - if (process.platform === 'darwin') { - for (const file of BINARY_FILES) { + for (const file of BINARY_FILES) { + if (process.platform === 'darwin') { // eslint-disable-next-line no-await-in-loop await macosVersionCheck(file); - } - } else if (process.platform === 'linux') { - for (const file of BINARY_FILES) { + } else if (process.platform === 'win32') { + // eslint-disable-next-line no-await-in-loop + await windowsDllImportCheck(file); + } else if (process.platform === 'linux') { // eslint-disable-next-line no-await-in-loop await linuxVersionCheck(file); }