From 04c622f6e9e02f7413332d6f0093f27534ed43a0 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 12 Nov 2026 06:18:04 +0800 Subject: [PATCH 1/4] fix(browser): preserve physical worker launch failures during cancellation (#10145) --- src/browser/WebBundleAnalysisWorker.test.ts | 11 +++++++++-- src/browser/WebBundleAnalysisWorker.ts | 6 +++++- src/process/OwnedWorkerRuntime.ts | 21 +++++++++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/browser/WebBundleAnalysisWorker.test.ts b/src/browser/WebBundleAnalysisWorker.test.ts index deda2fa0fe..a456727623 100644 --- a/src/browser/WebBundleAnalysisWorker.test.ts +++ b/src/browser/WebBundleAnalysisWorker.test.ts @@ -1259,8 +1259,8 @@ it.each(["typed", "physical"] as const)( const prepare = vi .spyOn(ownershipObservation, "prepareProcessOwnershipInspection") .mockResolvedValue(); - const launcher = vi.fn( - async (options: Parameters[0]) => { + const launcher = vi.fn( + async (options) => { expect(options.signal).toBeDefined(); controller.abort(reason); expect(options.signal?.reason).toBe(reason); @@ -1284,6 +1284,13 @@ it.each(["typed", "physical"] as const)( } expect(error.cause).toBe(reason); expect(launcher).toHaveBeenCalledOnce(); + launcher.mockImplementationOnce(launcherFor({ kind: "source" })); + const recovered = await worker.analyze( + makeScript("fetch('/after-admission');"), + 5_000, + ); + expect(recovered.ok).toBe(true); + expect(launcher).toHaveBeenCalledTimes(2); } finally { prepare.mockRestore(); await worker.close(); diff --git a/src/browser/WebBundleAnalysisWorker.ts b/src/browser/WebBundleAnalysisWorker.ts index 19bee960b9..167a0fef98 100644 --- a/src/browser/WebBundleAnalysisWorker.ts +++ b/src/browser/WebBundleAnalysisWorker.ts @@ -326,7 +326,11 @@ export class WebBundleAnalysisWorker { try { await this.#start(deadline); } catch (cause: unknown) { - if (processInspectionFailure(cause) !== undefined) + if ( + processInspectionFailure(cause) !== undefined || + (this.#runtime.launchFailure !== undefined && + Object.is(this.#runtime.launchFailure.cause, cause)) + ) ownershipInspectionFailure = this.#inspectionFailure(cause, deadline); throw cause; } diff --git a/src/process/OwnedWorkerRuntime.ts b/src/process/OwnedWorkerRuntime.ts index 50acd275f3..d053f46f94 100644 --- a/src/process/OwnedWorkerRuntime.ts +++ b/src/process/OwnedWorkerRuntime.ts @@ -152,6 +152,7 @@ export class OwnedWorkerRuntime { #lastRunId: string | undefined; #terminalCloseRequested = false; #inputFailure: Error | undefined; + #launchFailure: { readonly cause: unknown } | undefined; #stdinGeneration = 0; readonly #stdoutListeners = new Set<{ readonly stream: Readable; @@ -191,6 +192,11 @@ export class OwnedWorkerRuntime { return this.#inputFailure; } + /** Failure observed at the current launcher call, without reclassifying its cause. */ + get launchFailure(): { readonly cause: unknown } | undefined { + return this.#launchFailure; + } + /** Keep a returned typed cleanup error live until this exact owner releases. */ bindCleanupFailure(error: OwnedWorkerCleanupFailure | AnalysisError): void { if (this.#cleanupEpoch === undefined) { @@ -243,6 +249,7 @@ export class OwnedWorkerRuntime { const ownRetry = this.#registry.has(this) ? this.#retryPending() : Promise.resolve(); + this.#launchFailure = undefined; const acquisition = ownRetry.then(async () => { if (this.#closed) this.#closed = false; await this.#start(spawnOptions, initialize); @@ -356,10 +363,16 @@ export class OwnedWorkerRuntime { this.#cleanupEpoch ??= new OwnedWorkerRuntimeCleanupEpoch(this.#root.path); if (this.#closing) throw new ProcessAdmissionError("runtime_closing"); this.#lastRunId = spawnOptions.runId; - const spawned = await (this.options.launcher ?? spawnOwnedProviderProcess)({ - ...spawnOptions, - cwd: this.#root.path, - }); + let spawned: Awaited>; + try { + spawned = await (this.options.launcher ?? spawnOwnedProviderProcess)({ + ...spawnOptions, + cwd: this.#root.path, + }); + } catch (cause: unknown) { + this.#launchFailure = { cause }; + throw cause; + } this.#inputFailure = undefined; this.#observeStdin(spawned.process.stdin); this.#cleanupEpoch.setRunId(spawned.ownership?.runId ?? spawnOptions.runId); From bbef46e7830c935ff7983f05d981a72b7b26e555 Mon Sep 17 00:00:00 2001 From: 2SinceJuly Date: Thu, 12 Nov 2026 06:19:57 +0800 Subject: [PATCH 2/4] fix(hardening): recognize the __chk_fail FORTIFY handler import (#10036) * fix(hardening): recognize the __chk_fail FORTIFY handler import glibc exports __chk_fail as the shared failure handler that most FORTIFY checks funnel into, and its dynamic import is a positive sign that the binary uses FORTIFY. The ELF FORTIFY matcher accepted __fortify_fail and __*_chk names, so an image whose only relevant import was __chk_fail was reported as fortify: unknown with no evidence location. * style(hardening): wrap the __chk_fail fixture call to the formatter's width format:check fails on the current head because the new fixture call fits on one line at the file's previous width but exceeds oxfmt's 80-column limit with the longer fortifyName literal. --------- Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/native/hardening/BinaryHardeningParsers.test.ts | 13 +++++++++++++ src/native/hardening/ElfHardening.ts | 1 + 2 files changed, 14 insertions(+) diff --git a/src/native/hardening/BinaryHardeningParsers.test.ts b/src/native/hardening/BinaryHardeningParsers.test.ts index 5efc51d5ba..d92673743a 100644 --- a/src/native/hardening/BinaryHardeningParsers.test.ts +++ b/src/native/hardening/BinaryHardeningParsers.test.ts @@ -1108,6 +1108,19 @@ it("cites a non-ASCII import name by its source bytes", () => { expect(bytes.subarray(at, at + 7).toString("latin1")).toBe(name); }); +it("treats the __chk_fail handler import as a FORTIFY sign", () => { + const bytes = elfHardeningFixture({ + fortify: true, + fortifyName: "__chk_fail", + }); + const image = readElfHardening(bytes); + const row = findRow(image.rows, "fortify"); + expect(row).toMatchObject({ state: "enabled", location: { bytes: 10 } }); + expect(row.basis).toContain("__chk_fail"); + const at = row.location?.offset ?? 0; + expect(bytes.subarray(at, at + 10).toString("latin1")).toBe("__chk_fail"); +}); + it("keeps malformed name offsets malformed in both formats", async () => { await expect( readMachoHardening(machoHardeningFixture({ brokenStrx: true })), diff --git a/src/native/hardening/ElfHardening.ts b/src/native/hardening/ElfHardening.ts index 46f6d54b48..23a64065cf 100644 --- a/src/native/hardening/ElfHardening.ts +++ b/src/native/hardening/ElfHardening.ts @@ -279,6 +279,7 @@ export const readElfHardening = ( symbols, (name) => name === "__fortify_fail" || + name === "__chk_fail" || (name.startsWith("__") && name.endsWith("_chk")), "No fortify symbol was found in the dynamic symbol table. A static or stripped image can omit those imports while still using FORTIFY.", "The dynamic symbol table is absent, so FORTIFY use is unknown.", From 0c9ae2f783d918fbe23f50009ce61e96664c95b2 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:20:01 -0500 Subject: [PATCH 3/4] fix(native): normalize the CPU names lipo prints for i386 slices (#10136) cctools lipo prints CPU_TYPE_I386 and CPU_SUBTYPE_I386_ALL for i386 slices, so their CPU codes were unknown and every i386 slice carried unnormalized-text limitations. Co-authored-by: Cursor Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/native/parsers/lipo.test.ts | 21 +++++++++++++++++++++ src/native/parsers/lipo.ts | 2 ++ 2 files changed, 23 insertions(+) diff --git a/src/native/parsers/lipo.test.ts b/src/native/parsers/lipo.test.ts index 6e37cdea17..0282b49018 100644 --- a/src/native/parsers/lipo.test.ts +++ b/src/native/parsers/lipo.test.ts @@ -238,6 +238,27 @@ architecture arm64e ]); }); + it("normalizes the CPU names lipo prints for i386 slices", () => { + // cctools misc/lipo.c print_cputype() spells i386 with I386 names. + expect( + parseLipoOutput(`architecture i386 + cputype CPU_TYPE_I386 + cpusubtype CPU_SUBTYPE_I386_ALL + capabilities 0x0 + offset 16384 + size 28 + align 2^14 (16384) +`).architectures, + ).toMatchObject([ + { + name: "i386", + cpu_type_code: 7, + cpu_subtype_code: 3, + cpu_subtype_base: 3, + }, + ]); + }); + it("keeps unknown symbolic CPU values as source text without a code", () => { expect( parseLipoOutput( diff --git a/src/native/parsers/lipo.ts b/src/native/parsers/lipo.ts index 39ebbe371e..40315382cd 100644 --- a/src/native/parsers/lipo.ts +++ b/src/native/parsers/lipo.ts @@ -315,6 +315,7 @@ const integer = (value: string | undefined): number | null => { const CPU_TYPES: Readonly> = { CPU_TYPE_X86: 7, + CPU_TYPE_I386: 7, CPU_TYPE_X86_64: 0x01000007, CPU_TYPE_ARM: 12, CPU_TYPE_ARM64: 0x0100000c, @@ -325,6 +326,7 @@ const CPU_TYPES: Readonly> = { const CPU_SUBTYPES: Readonly> = { CPU_SUBTYPE_X86_ALL: 3, + CPU_SUBTYPE_I386_ALL: 3, CPU_SUBTYPE_X86_64_ALL: 3, CPU_SUBTYPE_X86_64_H: 8, CPU_SUBTYPE_ARM64_ALL: 0, From 166cba488a743f167060bcf679add036166b9399 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:20:06 -0500 Subject: [PATCH 4/4] fix(adb): classify mDNS wireless serials as TCP (#10134) Co-authored-by: Cursor Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/adb/AdbDeviceOutput.test.ts | 46 +++++++++++++++++++++++++++++++++ src/adb/AdbDeviceOutput.ts | 10 ++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/adb/AdbDeviceOutput.test.ts b/src/adb/AdbDeviceOutput.test.ts index 279ce3d723..a733b54dfa 100644 --- a/src/adb/AdbDeviceOutput.test.ts +++ b/src/adb/AdbDeviceOutput.test.ts @@ -74,6 +74,52 @@ describe("parseAdbDevicesOutput", () => { }, ]); }); + + it("classifies adb mDNS wireless serials as TCP without treating them as USB", () => { + const output = [ + "List of devices attached", + "adb-8c4f2a1b4e5d6f70-AbCdEf._adb-tls-connect._tcp device product:cheetah model:Pixel_7_Pro device:cheetah transport_id:4", + "phone._adb._tcp device transport_id:5", + "not-a-service._tcp device transport_id:6", + "adb-wired._adb-tls-connect._tcp device usb:1-1 product:raven model:Pixel_6 device:raven transport_id:7", + "", + ].join("\n"); + const { devices, unparsedLines } = parseAdbDevicesOutput(output); + expect(unparsedLines).toEqual({ sample: [], total: 0 }); + expect( + devices.map(({ serial, transport, kind, kind_basis }) => ({ + serial, + transport, + kind, + kind_basis, + })), + ).toEqual([ + { + serial: "adb-8c4f2a1b4e5d6f70-AbCdEf._adb-tls-connect._tcp", + transport: "tcp", + kind: "unknown", + kind_basis: "none", + }, + { + serial: "phone._adb._tcp", + transport: "tcp", + kind: "unknown", + kind_basis: "none", + }, + { + serial: "not-a-service._tcp", + transport: "unknown", + kind: "unknown", + kind_basis: "none", + }, + { + serial: "adb-wired._adb-tls-connect._tcp", + transport: "usb", + kind: "physical", + kind_basis: "usb_transport", + }, + ]); + }); }); describe("ADB daemon startup notices", () => { diff --git a/src/adb/AdbDeviceOutput.ts b/src/adb/AdbDeviceOutput.ts index 438c9aaef6..2253b1607e 100644 --- a/src/adb/AdbDeviceOutput.ts +++ b/src/adb/AdbDeviceOutput.ts @@ -23,6 +23,14 @@ import { parseAdbTransportIdentity } from "../domain/adb/adbTransportIdentity.js const DEVICE_LINE = /^(\S+)\s+(.*)$/u; +/** + * adb prints a wireless-debugging transport's serial as the mDNS instance + * `._adb._tcp` or `._adb-tls-connect._tcp`. That serial + * has neither a colon nor a `usb:` field. + */ +const isAdbMdnsTcpSerial = (serial: string): boolean => + serial.endsWith("._adb._tcp") || serial.endsWith("._adb-tls-connect._tcp"); + const normalizeAdbOutputLine = (rawLine: string): string => rawLine.trimEnd(); export const isSuccessfulDaemonStartNotice = (line: string): boolean => @@ -122,7 +130,7 @@ export const parseAdbDevicesOutput = ( transport: usb !== null ? "usb" - : isEmulator || serial.includes(":") + : isEmulator || serial.includes(":") || isAdbMdnsTcpSerial(serial) ? "tcp" : "unknown", product: metadataValue(remainder, "product"),