From 5acd3d86c8f08c5bee5d666249f51038f234123c Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 13 Mar 2026 23:22:27 +1100 Subject: [PATCH] =?UTF-8?q?fix:=20image=5Fscan=5Fhelper=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=A3=80=E6=B5=8B=20task=5Ffor=5Fpid=20=E6=9D=83?= =?UTF-8?q?=E9=99=90=E4=B8=8D=E8=B6=B3=E6=97=B6=E9=80=9A=E8=BF=87=20osascr?= =?UTF-8?q?ipt=20=E6=8F=90=E6=9D=83=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS SIP 下 ad-hoc 签名的 debugger entitlement 不被信任,导致 image_scan_helper 调用 task_for_pid 返回 kr=5。现在先尝试直接运行, 检测到权限错误后自动切换到 osascript with administrator privileges 方式运行 helper,后续调用跳过直接运行直接走提权路径。 Co-Authored-By: Claude Opus 4.6 --- electron/services/keyServiceMac.ts | 79 ++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/electron/services/keyServiceMac.ts b/electron/services/keyServiceMac.ts index 65394ee..0c9cd02 100644 --- a/electron/services/keyServiceMac.ts +++ b/electron/services/keyServiceMac.ts @@ -23,6 +23,7 @@ export class KeyServiceMac { private machVmRegion: any = null private machVmReadOverwrite: any = null private machPortDeallocate: any = null + private _needsElevation = false private getHelperPath(): string { const isPackaged = app.isPackaged @@ -634,30 +635,27 @@ export class KeyServiceMac { ciphertext: Buffer, onProgress?: (message: string) => void ): Promise { - // 优先通过 image_scan_helper 子进程调用(有 debugger entitlement) + // 优先通过 image_scan_helper 子进程调用 try { const helperPath = this.getImageScanHelperPath() const ciphertextHex = ciphertext.toString('hex') - const result = await new Promise((resolve, reject) => { - const child = spawn(helperPath, [String(pid), ciphertextHex], { stdio: ['ignore', 'pipe', 'pipe'] }) - let stdout = '' - child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() }) - child.stderr.on('data', (chunk: Buffer) => { console.log('[image_scan_helper]', chunk.toString().trim()) }) - child.on('error', reject) - child.on('close', () => { - try { - const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean) - const last = lines[lines.length - 1] - if (!last) { resolve(null); return } - const payload = JSON.parse(last) - resolve(payload?.success && payload?.aesKey ? payload.aesKey : null) - } catch { - resolve(null) - } - }) - setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, 30_000) - }) - if (result) return result + + // 1) 直接运行 helper(有正式签名的 debugger entitlement 时可用) + if (!this._needsElevation) { + const direct = await this._spawnScanHelper(helperPath, pid, ciphertextHex, false) + if (direct.key) return direct.key + if (direct.permissionError) { + console.warn('[KeyServiceMac] task_for_pid 权限不足,切换到 osascript 提权模式') + this._needsElevation = true + onProgress?.('需要管理员权限,请在弹出的对话框中输入密码...') + } + } + + // 2) 通过 osascript 以管理员权限运行 helper(SIP 下 ad-hoc 签名无法获取 task_for_pid) + if (this._needsElevation) { + const elevated = await this._spawnScanHelper(helperPath, pid, ciphertextHex, true) + if (elevated.key) return elevated.key + } } catch (e: any) { console.warn('[KeyServiceMac] image_scan_helper unavailable, fallback to Mach API:', e?.message) } @@ -757,6 +755,45 @@ export class KeyServiceMac { } } + private _spawnScanHelper( + helperPath: string, pid: number, ciphertextHex: string, elevated: boolean + ): Promise<{ key: string | null; permissionError: boolean }> { + return new Promise((resolve, reject) => { + let child: ReturnType + if (elevated) { + const shellCmd = `'${helperPath}' ${pid} ${ciphertextHex}` + child = spawn('osascript', ['-e', `do shell script ${JSON.stringify(shellCmd)} with administrator privileges`], + { stdio: ['ignore', 'pipe', 'pipe'] }) + } else { + child = spawn(helperPath, [String(pid), ciphertextHex], { stdio: ['ignore', 'pipe', 'pipe'] }) + } + const tag = elevated ? '[image_scan_helper:elevated]' : '[image_scan_helper]' + let stdout = '', stderr = '' + child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() }) + child.stderr.on('data', (chunk: Buffer) => { + stderr += chunk.toString() + console.log(tag, chunk.toString().trim()) + }) + child.on('error', reject) + child.on('close', () => { + const permissionError = !elevated && stderr.includes('task_for_pid failed') + try { + const lines = stdout.split(/\r?\n/).map(x => x.trim()).filter(Boolean) + const last = lines[lines.length - 1] + if (!last) { resolve({ key: null, permissionError }); return } + const payload = JSON.parse(last) + resolve({ + key: payload?.success && payload?.aesKey ? payload.aesKey : null, + permissionError + }) + } catch { + resolve({ key: null, permissionError }) + } + }) + setTimeout(() => { try { child.kill('SIGTERM') } catch {} }, elevated ? 60_000 : 30_000) + }) + } + private async findWeChatPid(): Promise { const { execSync } = await import('child_process') try {