mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-02 08:13:37 +01:00
346 lines
9.3 KiB
TypeScript
346 lines
9.3 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
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';
|
|
|
|
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
|
|
const MACOS_TO_DARWIN_VERSIONS = new Map([
|
|
// Big Sur
|
|
['11.0', '20.1.0'],
|
|
['11.1', '20.2.0'],
|
|
['11.2', '20.3.0'],
|
|
['11.3', '20.4.0'],
|
|
['11.4', '20.5.0'],
|
|
|
|
// Monterey
|
|
['12.0', '21.0.1'],
|
|
['12.0.1', '21.1.0'],
|
|
['12.1', '21.2.0'],
|
|
['12.2', '21.3.0'],
|
|
['12.3', '21.4.0'],
|
|
['12.4', '21.5.0'],
|
|
['12.5', '21.6.0'],
|
|
|
|
// Ventura
|
|
['13.0', '22.1.0'],
|
|
['13.1', '22.2.0'],
|
|
['13.2', '22.3.0'],
|
|
['13.3', '22.4.0'],
|
|
['13.4', '22.5.0'],
|
|
['13.5', '22.6.0'],
|
|
|
|
// Sonoma
|
|
['14.0', '23.0.0'],
|
|
['14.1', '23.1.0'],
|
|
['14.2', '23.2.0'],
|
|
['14.3', '23.3.0'],
|
|
['14.4', '23.4.0'],
|
|
['14.5', '23.5.0'],
|
|
|
|
// Sequoia
|
|
['15.0', '24.0.0'],
|
|
|
|
// Tahoe
|
|
['26.0', '25.0.0'],
|
|
]);
|
|
|
|
async function macosVersionCheck(file: string) {
|
|
console.log(`${file}: checking...`);
|
|
|
|
const { stdout } = await execFile('otool', ['-l', file]);
|
|
|
|
type MacosMatch = RegExpMatchArray & { 1: string };
|
|
const match = stdout.match(/minos\s+([\d.]+)/);
|
|
if (match == null) {
|
|
throw new Error(`Failed to detect min OS version of ${file}`);
|
|
}
|
|
|
|
const [, macosVersion] = match as MacosMatch;
|
|
const darwinVersion = MACOS_TO_DARWIN_VERSIONS.get(macosVersion);
|
|
if (darwinVersion == null) {
|
|
throw new Error(`No matching darwin version for macOS ${macosVersion}`);
|
|
}
|
|
|
|
const minSupported = packageJson.build.mac.releaseInfo.vendor.minOSVersion;
|
|
if (gte(minSupported, darwinVersion)) {
|
|
console.log(`${file}: required version ${darwinVersion}`);
|
|
return;
|
|
}
|
|
|
|
throw new Error(
|
|
`${basename(file)} minimum darwin version is ${darwinVersion} ` +
|
|
`(macOS ${macosVersion}), but package.json has ${minSupported}`
|
|
);
|
|
}
|
|
|
|
// 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<void> {
|
|
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<ArrayBuffer>, 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<string>();
|
|
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`;
|
|
}
|
|
if (/^\d+\.\d+\.\d+$/.test(version)) {
|
|
return version;
|
|
}
|
|
throw new Error(`Unsupported glibc version: ${version}`);
|
|
}
|
|
|
|
async function linuxVersionCheck(file: string) {
|
|
if (!existsSync(file)) {
|
|
console.log(`${file}: skipping`);
|
|
return;
|
|
}
|
|
|
|
console.log(`${file}: checking...`);
|
|
|
|
const { stdout } = await execFile('objdump', ['-T', file], {
|
|
maxBuffer: 100 * 1024 * 1024,
|
|
});
|
|
|
|
let minGlibcVersion: string | undefined;
|
|
type GlibcMatch = RegExpExecArray & { 1: string };
|
|
for (const match of stdout.matchAll(/GLIBC_([\d.]+)/g)) {
|
|
const [, unpaddedVersion] = match as GlibcMatch;
|
|
const glibcVersion = padGlibcVersion(unpaddedVersion);
|
|
if (minGlibcVersion == null || gte(glibcVersion, minGlibcVersion)) {
|
|
minGlibcVersion = glibcVersion;
|
|
}
|
|
}
|
|
|
|
if (minGlibcVersion == null) {
|
|
throw new Error(`Failed to detect glibc versions of ${file}`);
|
|
}
|
|
|
|
const libc6Dependency = packageJson.build.deb.depends.find(req =>
|
|
req.startsWith('libc6 ')
|
|
);
|
|
if (libc6Dependency == null) {
|
|
throw new Error('Missing libc6 dependency in package.json');
|
|
}
|
|
|
|
type Libc6Match = RegExpMatchArray & { 1: string };
|
|
const match = libc6Dependency.match(/^libc6 \(>= ([\d.]+)\)$/);
|
|
if (match == null) {
|
|
throw new Error(
|
|
`Invalid libc6 dependency in package.json, ${libc6Dependency}`
|
|
);
|
|
}
|
|
|
|
const [, unpaddedVersion] = match as Libc6Match;
|
|
const minSupported = padGlibcVersion(unpaddedVersion);
|
|
if (gte(minSupported, minGlibcVersion)) {
|
|
console.log(`${file}: required version ${minGlibcVersion}`);
|
|
return;
|
|
}
|
|
|
|
throw new Error(
|
|
`${basename(file)} minimum GLIBC version is ${minGlibcVersion} ` +
|
|
`but package.json has ${libc6Dependency}`
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
const BINARY_FILES = [
|
|
ELECTRON_BINARY as unknown as string,
|
|
...(await fastGlob(
|
|
packageJson.build.files
|
|
.filter((p: unknown): p is string => typeof p === 'string')
|
|
.filter(p => p.endsWith('.node'))
|
|
.map(p => p.replace(/\${platform}/, process.platform))
|
|
.map(p => p.replace(/\${arch}/, process.arch)),
|
|
{
|
|
absolute: true,
|
|
onlyFiles: true,
|
|
cwd: join(__dirname, '..', '..'),
|
|
}
|
|
)),
|
|
];
|
|
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 === '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);
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(main());
|