Skip to content

Commit f9a9603

Browse files
committed
Close 31 BUGHUNTERCLAUDE findings: EFI correctness, security, runtime (v3.2.0)
CRITICAL fixes: - C1: AMD laptop now uses laptop-aware EC SSDT (was hardcoded desktop variant) - C3: getFreeSpaceMB validates drive letter is single ASCII char before PS interpolation - C5: Added Wolfdale/Yorkfield/Nehalem/Westmere/Arrandale/Clarkdale to Tahoe unsupported set HIGH fixes: - H1: open-folder IPC uses shell.showItemInFolder instead of shell.openPath - H3: isSafeExternalTarget rejects http:// URLs (https:// and mailto: only) - H4: Update download resume: removed unconditional temp file delete before resume check - H5: BIOS managed mode now reachable (added 'managed' to BiosApplyMode type and logic) - H6: Apple recovery URLs changed from http:// to https:// - H7: Added Haswell-E/Broadwell-E/Ivy Bridge-E to macOS 12 cap in compatibility engine - H8: Z390 boards now set SetupVirtualMap=false - H9: Fixed regex [2|1] → [12] in Pentium/Celeron generation detection - H10: Added VALID_DEVICE_PATTERN validation for device strings in flash IPC handlers MEDIUM fixes: - M1: Apple recovery session tokens use crypto.randomBytes instead of Math.random - M2: probeUrl redirect depth limited to 5 hops - M3: Apple recovery metadata response body capped at 10MB - M7: Ice Lake iGPU correctly identified as Iris Plus (not UHD 630) - M8: Broadwell desktop headless ig-platform-id fixed (0x16260004 not mobile 0x16260006) - M10: save-state IPC validates AppState shape before persisting - M11: GitHub rate limit response body capped at 64KB - M12: formatBytes handles bytes<=0 and TB+ values without NaN - M13: Linux virtualization detection uses LANG=C for locale independence - M14: selectOverallSupportLevel returns minimum (not maximum) support level - M15: Windows automount re-enabled in before-quit handler (crash safety) LOW fixes: - L1: CPUID base64 padded to 16 bytes (was 15) - L2: Layout-id encoding uses Buffer.from for proper little-endian (not btoa) - L5: cleanupOrphanedBuilds uses async fs operations - L6: Build directory names include random hex suffix for collision safety - L7: retryWithBackoff guards against maxAttempts <= 0 - L8: Removed dead Set-Cookie header check (Node.js lowercases headers) - L9: Removed duplicate motherboard variable declaration SECURITY fixes: - S2: probeUrl rejects HTTPS→HTTP redirect downgrades Updated 6 test files to match corrected behavior.
1 parent 743ca34 commit f9a9603

18 files changed

Lines changed: 617 additions & 63 deletions

BUGHUNTERCLAUDE.md

Lines changed: 511 additions & 0 deletions
Large diffs are not rendered by default.

electron/appleRecovery.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import * as crypto from 'node:crypto';
12
import * as http from 'node:http';
23
import * as https from 'node:https';
34

45
export const APPLE_RECOVERY_HOST = 'osrecovery.apple.com';
56
export const INTERNET_RECOVERY_USER_AGENT = 'InternetRecovery/1.0';
6-
export const APPLE_RECOVERY_ROOT_URL = `http://${APPLE_RECOVERY_HOST}/`;
7-
export const APPLE_RECOVERY_IMAGE_URL = `http://${APPLE_RECOVERY_HOST}/InstallationPayload/RecoveryImage`;
7+
export const APPLE_RECOVERY_ROOT_URL = `https://${APPLE_RECOVERY_HOST}/`;
8+
export const APPLE_RECOVERY_IMAGE_URL = `https://${APPLE_RECOVERY_HOST}/InstallationPayload/RecoveryImage`;
89
export const APPLE_RECOVERY_MLB_ZERO = '00000000000000000';
910

1011
export interface AppleRecoveryEndpointProbeResult {
@@ -36,12 +37,7 @@ export type AppleRecoveryTransport = (input: {
3637
}) => Promise<AppleRecoveryHttpResponse>;
3738

3839
function randomHex(length: number): string {
39-
const alphabet = '0123456789ABCDEF';
40-
let value = '';
41-
for (let i = 0; i < length; i += 1) {
42-
value += alphabet[Math.floor(Math.random() * alphabet.length)];
43-
}
44-
return value;
40+
return crypto.randomBytes(length).toString('hex').toUpperCase().slice(0, length);
4541
}
4642

4743
function readSessionCookie(
@@ -135,9 +131,15 @@ export async function createAppleRecoveryTransport(input: {
135131
headers: input.headers,
136132
timeout: input.timeoutMs ?? 15000,
137133
}, (res) => {
134+
const MAX_METADATA_BODY = 10 * 1024 * 1024;
138135
let body = '';
139136
res.on('data', (chunk: Buffer) => {
140137
body += chunk.toString('utf-8');
138+
if (body.length > MAX_METADATA_BODY) {
139+
res.destroy();
140+
reject(new Error('Response body too large'));
141+
return;
142+
}
141143
});
142144
res.on('end', () => {
143145
resolve({
@@ -179,7 +181,7 @@ export async function probeAppleRecoveryEndpoint(
179181
return {
180182
reachable: response.statusCode > 0 && response.statusCode < 500,
181183
httpCode: response.statusCode,
182-
sessionCookie: readSessionCookie(response.headers['set-cookie']) ?? readSessionCookie(response.headers['Set-Cookie']),
184+
sessionCookie: readSessionCookie(response.headers['set-cookie']),
183185
};
184186
} catch {
185187
return {

electron/bios/orchestrator.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,19 +215,22 @@ function getAllowedApplyModes(supportLevel: BiosSupportLevel, safeMode: boolean)
215215
if (supportLevel === 'assisted' || (supportLevel === 'managed' && safeMode)) {
216216
return ['manual', 'assisted', 'skipped'];
217217
}
218+
if (supportLevel === 'managed' && !safeMode) {
219+
return ['manual', 'assisted', 'managed', 'skipped'];
220+
}
218221
return ['manual', 'assisted', 'skipped'];
219222
}
220223

221224
function getDefaultApplyMode(supportLevel: BiosSupportLevel, safeMode: boolean): BiosApplyMode {
222225
if (supportLevel === 'manual') return 'manual';
223226
if (safeMode) return 'manual';
224-
return supportLevel === 'managed' ? 'assisted' : 'assisted';
227+
return supportLevel === 'managed' ? 'managed' : 'assisted';
225228
}
226229

227230
function selectOverallSupportLevel(levels: BiosSupportLevel[]): BiosSupportLevel {
228-
if (levels.includes('managed')) return 'managed';
231+
if (levels.includes('manual')) return 'manual';
229232
if (levels.includes('assisted')) return 'assisted';
230-
return 'manual';
233+
return 'managed';
231234
}
232235

233236
export function createDefaultSelections(settings: Pick<BiosSettingPlan, 'id' | 'supportLevel'>[], safeMode: boolean): Record<BiosSettingId, BiosSettingSelection> {

electron/bios/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { FirmwareInfo } from '../firmwarePreflight.js';
44
export type BiosVendor = 'Generic' | 'HP' | 'Dell' | 'Lenovo';
55
export type BiosBackendId = 'generic' | 'hp' | 'dell' | 'lenovo';
66
export type BiosSupportLevel = 'manual' | 'assisted' | 'managed';
7-
export type BiosApplyMode = 'manual' | 'assisted' | 'skipped';
7+
export type BiosApplyMode = 'manual' | 'assisted' | 'managed' | 'skipped';
88

99
// Extended session stage — matches BiosFlowState in stateMachine.ts
1010
export type BiosSessionStage =

electron/configGenerator.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,10 @@ export function getQuirksForGeneration(gen: HardwareProfile['generation'], mothe
452452
quirks.DevirtualiseMmio = true;
453453
quirks.AppleCpuPmCfgLock = false;
454454
quirks.SetupVirtualMap = true;
455-
// Z390 boards need ProtectUefiServices — Source: config.plist/coffee-lake.html
455+
// Z390 boards need ProtectUefiServices and must disable SetupVirtualMap — Source: config.plist/coffee-lake.html
456456
if (mb.includes('z390')) {
457457
quirks.ProtectUefiServices = true;
458+
quirks.SetupVirtualMap = false;
458459
}
459460
break;
460461
case 'Comet Lake':
@@ -788,8 +789,7 @@ export function getRequiredResources(profile: HardwareProfile) {
788789
// Will be conditionally added if Broadcom card detected
789790
}
790791

791-
const mb = profile.motherboard.toLowerCase();
792-
const needsAmdCpuSsdt = /\b(a520|b550|a620|b650|x670|x670e|b850|x870|x870e)\b/.test(mb);
792+
const needsAmdCpuSsdt = /\b(a520|b550|a620|b650|x670|x670e|b850|x870|x870e)\b/.test(motherboard);
793793

794794
// SSDTs by platform — Source: per-gen config.plist pages
795795
// Laptops use SSDT-EC-USBX-LAPTOP.aml instead of the desktop variant.
@@ -809,15 +809,15 @@ export function getRequiredResources(profile: HardwareProfile) {
809809
pushSsdt(ecUsbxSsdt);
810810
// SSDT-PMC required for 300-series boards (Z370/Z390/H370/B360/H310) for native NVRAM
811811
// Source: config.plist/coffee-lake.html — "Required for all 300-series motherboards"
812-
if (!profile.isLaptop && (mb.includes('z390') || mb.includes('z370') || mb.includes('h370') || mb.includes('b360') || mb.includes('b365') || mb.includes('h310') || mb.includes('q370'))) {
812+
if (!profile.isLaptop && (motherboard.includes('z390') || motherboard.includes('z370') || motherboard.includes('h370') || motherboard.includes('b360') || motherboard.includes('b365') || motherboard.includes('h310') || motherboard.includes('q370'))) {
813813
pushSsdt('SSDT-PMC.aml');
814814
}
815815
// Laptop Coffee Lake+ also needs SSDT-PMC for 300-series mobile chipsets
816816
if (profile.isLaptop && ['Coffee Lake'].includes(profile.generation)) {
817817
pushSsdt('SSDT-PMC.aml');
818818
}
819819
// SSDT-RHUB: USB root hub reset required on Z490 Comet Lake boards — Source: Dortania comet-lake.html
820-
if (!profile.isLaptop && profile.generation === 'Comet Lake' && mb.includes('z490')) {
820+
if (!profile.isLaptop && profile.generation === 'Comet Lake' && motherboard.includes('z490')) {
821821
pushSsdt('SSDT-RHUB.aml');
822822
}
823823
} else if (['Haswell', 'Broadwell'].includes(profile.generation)) {
@@ -860,7 +860,7 @@ export function getRequiredResources(profile: HardwareProfile) {
860860
pushSsdt('SSDT-EC.aml');
861861
}
862862
} else if (profile.architecture === 'AMD') {
863-
pushSsdt('SSDT-EC-USBX-DESKTOP.aml');
863+
pushSsdt(ecUsbxSsdt);
864864
if (needsAmdCpuSsdt) {
865865
pushSsdt('SSDT-CPUR.aml');
866866
}
@@ -875,6 +875,7 @@ export function getRequiredResources(profile: HardwareProfile) {
875875
// required by the macOS 26 kernel and are not supported by any valid SMBIOS on Tahoe.
876876
// Source: Dortania tahoe.html compatibility table.
877877
const TAHOE_UNSUPPORTED_GENERATIONS = new Set<HardwareProfile['generation']>([
878+
'Wolfdale', 'Yorkfield', 'Nehalem', 'Westmere', 'Arrandale', 'Clarkdale',
878879
'Penryn', 'Sandy Bridge', 'Ivy Bridge', 'Haswell', 'Broadwell',
879880
]);
880881

@@ -949,8 +950,8 @@ export function generateConfigPlist(profile: HardwareProfile): string {
949950
}
950951

951952
// CPUID spoofing for unsupported Intel gens
952-
let cpuid1Data = "AAAAAAAAAAAAAAAAAAAA";
953-
let cpuid1Mask = "AAAAAAAAAAAAAAAAAAAA";
953+
let cpuid1Data = "AAAAAAAAAAAAAAAAAAAAAA==";
954+
let cpuid1Mask = "AAAAAAAAAAAAAAAAAAAAAA==";
954955
// Rocket Lake (11th gen) needs Comet Lake CPUID spoof — unsupported CPUID in macOS
955956
// Alder/Raptor Lake also need spoofing — Source: Dortania per-gen guides
956957
if (['Rocket Lake', 'Alder Lake', 'Raptor Lake'].includes(profile.generation)) {
@@ -1031,7 +1032,7 @@ export function generateConfigPlist(profile: HardwareProfile): string {
10311032
'Sandy Bridge':'AAAFAA==', // 0x00050000
10321033
'Ivy Bridge': 'BwBiAQ==', // 0x01620007
10331034
'Haswell': 'BAASBA==', // 0x04120004
1034-
'Broadwell': 'BgAmFg==', // 0x16260006 (no Dortania-specified desktop headless; using mobile fallback)
1035+
'Broadwell': 'BAAmFg==', // 0x16260004 — Dortania desktop headless
10351036
'Skylake': 'AQASGQ==', // 0x19120001
10361037
'Kaby Lake': 'AwASWQ==', // 0x59120003
10371038
'Coffee Lake': 'AwCRPg==', // 0x3E910003
@@ -1093,7 +1094,7 @@ export function generateConfigPlist(profile: HardwareProfile): string {
10931094
: 'PciRoot(0x0)/Pci(0x1b,0x0)';
10941095

10951096
// Audio layout-id as base64
1096-
const layoutIdBase64 = btoa(String.fromCharCode(audioLayoutId, 0, 0, 0));
1097+
const layoutIdBase64 = Buffer.from([audioLayoutId & 0xFF, (audioLayoutId >> 8) & 0xFF, 0, 0]).toString('base64');
10971098

10981099
// ── ACPI Delete entries ──────────────────────────────────────────────────
10991100
// Sandy Bridge / Ivy Bridge: delete CpuPm and Cpu0Ist ACPI tables

electron/diskOps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ async function getFreeSpaceMB(targetPath: string): Promise<number> {
9898
try {
9999
if (process.platform === 'win32') {
100100
const drive = targetPath.split(':')[0];
101+
if (!/^[A-Za-z]$/.test(drive)) return 0;
101102
const { stdout } = await execPromise(`powershell -NoProfile -Command "(Get-PSDrive -Name '${drive}' -ErrorAction SilentlyContinue).Free"`);
102103
return Math.floor(parseInt(stdout.trim()) / 1024 / 1024) || 0;
103104
} else {

electron/efiBuildFlow.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
3+
import crypto from 'node:crypto';
34
import type { HardwareProfile } from './configGenerator.js';
45
import type { OpToken } from './taskManager.js';
56

@@ -53,15 +54,15 @@ export interface RunEfiBuildFlowInput {
5354
allowAcceptedSession?: boolean;
5455
}
5556

56-
export function cleanupOrphanedBuilds(userDataPath: string, keepPath?: string): number {
57+
export async function cleanupOrphanedBuilds(userDataPath: string, keepPath?: string): Promise<number> {
5758
let removed = 0;
5859
try {
59-
const entries = fs.readdirSync(userDataPath, { withFileTypes: true });
60+
const entries = await fs.promises.readdir(userDataPath, { withFileTypes: true });
6061
for (const entry of entries) {
6162
if (!entry.isDirectory() || !entry.name.startsWith('EFI_Build_')) continue;
6263
const full = path.join(userDataPath, entry.name);
6364
if (keepPath && full === keepPath) continue;
64-
try { fs.rmSync(full, { recursive: true, force: true }); removed++; } catch (_) {}
65+
try { await fs.promises.rm(full, { recursive: true, force: true }); removed++; } catch (_) {}
6566
}
6667
} catch (_) {}
6768
return removed;
@@ -73,7 +74,7 @@ export async function runEfiBuildFlow(
7374
): Promise<string> {
7475
const { profile, allowAcceptedSession } = input;
7576
const token = deps.registry.create('efi-build');
76-
const efiPath = path.resolve(deps.getUserDataPath(), 'EFI_Build_' + Date.now());
77+
const efiPath = path.resolve(deps.getUserDataPath(), `EFI_Build_${Date.now()}-${crypto.randomBytes(4).toString('hex')}`);
7778
deps.log('INFO', 'efi', 'Building EFI', {
7879
efiPath,
7980
cpu: profile.cpu,
@@ -114,7 +115,7 @@ export async function runEfiBuildFlow(
114115
throw deps.createClassifiedIpcError(classified, error);
115116
}
116117

117-
cleanupOrphanedBuilds(deps.getUserDataPath(), efiPath);
118+
await cleanupOrphanedBuilds(deps.getUserDataPath(), efiPath);
118119
if (!fs.existsSync(efiPath)) fs.mkdirSync(efiPath, { recursive: true });
119120

120121
try {

electron/hackintoshRules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export function parseMacOSVersion(os: string): number {
5858
}
5959

6060
const versionMatch = lower.match(/(\d+(?:\.\d+)?)/);
61+
// Default to 15 (Sequoia) — the current target version. Returning null would
62+
// require updating all callers that assume a numeric return. Intentional.
6163
return versionMatch ? parseFloat(versionMatch[1]) : 15;
6264
}
6365

electron/hardwareDetect.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ export function inferIntelIgpuName(cpuName: string): string | null {
420420
const num = parseInt(match[1]);
421421
if (num >= 12000) return 'Intel UHD Graphics (12th Gen+)';
422422
if (num >= 11000) return 'Intel UHD Graphics (11th Gen)';
423+
if (num >= 10000 && num < 11000 && (/\bg[147]\b/.test(model) || model.includes('ice lake'))) return 'Intel Iris Plus Graphics (Ice Lake)';
423424
if (num >= 10000) return 'Intel UHD Graphics 630';
424425
if (num >= 8000) return 'Intel UHD Graphics 630';
425426
if (num >= 7000) return 'Intel HD Graphics 620/630';

electron/hardwareMapper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function detectCpuGeneration(cpuModel: string): HardwareProfile['generati
6161
if (model.includes('gold')) return 'Coffee Lake';
6262
if (model.match(/g[45]\d{2}/)) return 'Skylake';
6363
if (model.match(/g3\d{2}/)) return 'Haswell';
64-
if (model.match(/g[2|1]\d{2}/) || model.match(/g[68]\d0/)) return 'Sandy Bridge';
64+
if (model.match(/g[12]\d{2}/) || model.match(/g[68]\d0/)) return 'Sandy Bridge';
6565
return 'Ivy Bridge';
6666
}
6767

0 commit comments

Comments
 (0)