From d39699ad0c38e2f0a56c876407467efa546e83a2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:47:18 +0800 Subject: [PATCH] fix(validation): stop lexical failures before semantic conversions (#9924) --- .../BinaryLayoutService.test.ts | 116 +++++++++--------- .../RecordedCrashService.test.ts | 4 + src/domain/desktop/appBuild.test.ts | 2 + src/domain/desktop/appDownloadInstall.ts | 2 +- src/domain/native/binaryLayout.ts | 4 +- src/domain/native/recordedCrash.ts | 4 +- 6 files changed, 73 insertions(+), 59 deletions(-) diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts index c087a83a64..660165d28c 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts @@ -157,60 +157,64 @@ it("retains the acquired observation when cancellation arrives during inspection } }); -it.each(["different-path", "invalid-digest", "outside-range"])( - "rejects provider evidence at the next boundary: %s", - async (problem) => { - const fixture = binaryLayoutFixture(); - if (problem === "different-path") fixture.artifact.path = "/different.elf"; - if (problem === "invalid-digest") fixture.artifact.sha256 = "unobserved"; - if (problem === "outside-range") - fixture.segments.push({ - index: 0, - type: "PT_LOAD", - offset: "0x80", - header_location: { offset: "0x0", bytes: "0x38" }, - file_backing: "file", - file_size: "0x1", - memory_size: "0x1", - virtual_address: "0x20000000000001", - physical_address: "0x0", - alignment: "0x1", - flags: "0x5", - permissions: { read: true, write: false, execute: true }, - }); - const service = new BinaryLayoutService({ - inspectAndClose(input, options) { - return this.inspect(input, options); - }, - identity: BINARY_LAYOUT_TEST_PROVIDER, - inspect: () => Promise.resolve(ok(fixture)), +it.each([ + "different-path", + "invalid-digest", + "outside-range", + "invalid-hex", + "fractional-bytes", +])("rejects provider evidence at the next boundary: %s", async (problem) => { + const fixture = binaryLayoutFixture(); + if (problem === "different-path") fixture.artifact.path = "/different.elf"; + if (problem === "invalid-digest") fixture.artifact.sha256 = "unobserved"; + if (problem === "fractional-bytes") fixture.artifact.bytes = 1.5; + if (problem === "outside-range" || problem === "invalid-hex") + fixture.segments.push({ + index: 0, + type: "PT_LOAD", + offset: problem === "invalid-hex" ? "not-hex" : "0x80", + header_location: { offset: "0x0", bytes: "0x38" }, + file_backing: "file", + file_size: "0x1", + memory_size: "0x1", + virtual_address: "0x20000000000001", + physical_address: "0x0", + alignment: "0x1", + flags: "0x5", + permissions: { read: true, write: false, execute: true }, }); - expect( - await service.inspect({ path: "/artifacts/source-owned.elf" }), - ).toMatchObject({ - ok: false, - error: { - _tag: "AnalysisOutputError", - capturedOutput: fixture.diagnostics, - }, - }); - const controller = new AbortController(); - const lateInvalid = await new BinaryLayoutService({ - inspectAndClose(input, options) { - return this.inspect(input, options); - }, - identity: BINARY_LAYOUT_TEST_PROVIDER, - inspect: () => { - controller.abort(); - return Promise.resolve(ok(fixture)); - }, - }).inspect( - { path: "/artifacts/source-owned.elf" }, - { signal: controller.signal }, - ); - expect(lateInvalid).toMatchObject({ - ok: false, - error: { _tag: "AnalysisOutputError" }, - }); - }, -); + const service = new BinaryLayoutService({ + inspectAndClose(input, options) { + return this.inspect(input, options); + }, + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(fixture)), + }); + expect( + await service.inspect({ path: "/artifacts/source-owned.elf" }), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisOutputError", + capturedOutput: fixture.diagnostics, + }, + }); + const controller = new AbortController(); + const lateInvalid = await new BinaryLayoutService({ + inspectAndClose(input, options) { + return this.inspect(input, options); + }, + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => { + controller.abort(); + return Promise.resolve(ok(fixture)); + }, + }).inspect( + { path: "/artifacts/source-owned.elf" }, + { signal: controller.signal }, + ); + expect(lateInvalid).toMatchObject({ + ok: false, + error: { _tag: "AnalysisOutputError" }, + }); +}); diff --git a/src/application/binaryDiagnostics/RecordedCrashService.test.ts b/src/application/binaryDiagnostics/RecordedCrashService.test.ts index f9d723c5d1..bb9783b742 100644 --- a/src/application/binaryDiagnostics/RecordedCrashService.test.ts +++ b/src/application/binaryDiagnostics/RecordedCrashService.test.ts @@ -169,6 +169,8 @@ it.each([ "outside-range", "owner-base64", "descriptor-base64", + "invalid-hex", + "fractional-bytes", ])("rejects provider evidence at the next boundary: %s", async (problem) => { const fixture = recordedCrashFixture(); if (problem === "different-path") fixture.artifact.path = "/different.elf"; @@ -176,6 +178,8 @@ it.each([ if (problem === "owner-base64") fixture.notes[0]!.owner_bytes_base64 = "A"; if (problem === "descriptor-base64") fixture.notes[0]!.descriptor_bytes_base64 = "A"; + if (problem === "invalid-hex") fixture.segments[0]!.offset = "not-hex"; + if (problem === "fractional-bytes") fixture.artifact.bytes = 1.5; if (problem === "outside-range") fixture.segments.push({ index: 0, diff --git a/src/domain/desktop/appBuild.test.ts b/src/domain/desktop/appBuild.test.ts index 8963b8dd75..7b6d4910e1 100644 --- a/src/domain/desktop/appBuild.test.ts +++ b/src/domain/desktop/appBuild.test.ts @@ -260,6 +260,8 @@ describe("build bundle download installs", () => { }, }).success; expect(install({})).toBe(true); + expect(install({ url: "not-a-url" })).toBe(false); + expect(install({ url: "https://" })).toBe(false); expect(install({ program: "../bin/sh" })).toBe(false); expect(install({ program: "/usr/bin/notes" })).toBe(false); expect(install({ program: "notes//notes" })).toBe(false); diff --git a/src/domain/desktop/appDownloadInstall.ts b/src/domain/desktop/appDownloadInstall.ts index d0fa6d62b0..914f8a973f 100644 --- a/src/domain/desktop/appDownloadInstall.ts +++ b/src/domain/desktop/appDownloadInstall.ts @@ -7,7 +7,7 @@ export const COMPUTER_ARCHITECTURES = ["amd64", "arm64"] as const; /** A URL agentenv downloads from: http or https in lowercase, without a user name or password. */ export const downloadUrlSchema = z - .url({ protocol: /^https?$/u }) + .url({ protocol: /^https?$/u, abort: true }) .regex(/^https?:\/\//u) .refine( (url) => { diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts index a129168d05..84ac95eb50 100644 --- a/src/domain/native/binaryLayout.ts +++ b/src/domain/native/binaryLayout.ts @@ -6,7 +6,9 @@ import { pwntoolsProducerLimitsField } from "./pwntoolsResourceLimits.js"; import { absoluteLocalPathStringSchema } from "../localPath.js"; import { canonicalBase64Schema } from "../stringPatterns.js"; -const unsignedHex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/); +const unsignedHex = z + .string() + .regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/, { abort: true }); const index = z.int().nonnegative(); const scalar = z.union([z.string(), z.int()]); const range = z.strictObject({ offset: unsignedHex, bytes: unsignedHex }); diff --git a/src/domain/native/recordedCrash.ts b/src/domain/native/recordedCrash.ts index f1fa165e80..13a981b232 100644 --- a/src/domain/native/recordedCrash.ts +++ b/src/domain/native/recordedCrash.ts @@ -7,7 +7,9 @@ import { absoluteLocalPathStringSchema } from "../localPath.js"; import { canonicalBase64Schema } from "../stringPatterns.js"; import { recordedNoteCoverageIssues } from "./recordedCrashNoteCoverage.js"; -const hex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/); +const hex = z + .string() + .regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/, { abort: true }); const index = z.int().nonnegative(); const signed32 = z.int().min(-2147483648).max(2147483647); const base64 = canonicalBase64Schema;