From 25bea07b42119f5bb2f17719ffcdea9e37aa40ce Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:59:18 +0800 Subject: [PATCH] fix(model): preserve unsupported GGUF version diagnostics (#5789) Preserve unsupported GGUF version bytes and both UINT32 interpretations without inventing byte order at the shared parser. Keep existing layout support, typed errors, evidence identity, scoped reads and resource limits. Validation: 78 focused tests, full static/architecture and documentation checks, real CLI/MCP version controls and an independently checked public model. Latest-head Codex found no issues; Greptile is 5/5 and all review threads are resolved. Some CI remained pending at merge qualification. Merge under the human owner's explicit standing bypass authorization does not claim the full CI suite passed. Fixes #5749. --- docs/model-artifacts.md | 5 +++ src/model/GgufHeader.ts | 19 +++++--- tests/boundary/model/modelArtifact.test.ts | 51 ++++++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/docs/model-artifacts.md b/docs/model-artifacts.md index 59b5da04f1..f59c2c7a32 100644 --- a/docs/model-artifacts.md +++ b/docs/model-artifacts.md @@ -5,6 +5,11 @@ GGUF v2/v3 or safetensors file. Detection uses content, including GGUF byte order, rather than its extension. REA does not load a model, interpret weights, run inference, import Python, open external-data references or use the network. +Unsupported GGUF versions remain refused. Their diagnostic preserves the four +version bytes and both UINT32 interpretations with byte order unknown, rather +than inferring big-endian from an unrecognized little-endian value. Little-endian +v2/v3 and big-endian v3 remain supported; big-endian v2 remains refused. + ```bash rea inspect-model-artifact /absolute/path/model.gguf --json rea inspect-model-artifact /absolute/path/model.safetensors --hash full --json diff --git a/src/model/GgufHeader.ts b/src/model/GgufHeader.ts index d30c109466..1f2899f82c 100644 --- a/src/model/GgufHeader.ts +++ b/src/model/GgufHeader.ts @@ -247,16 +247,21 @@ export class GgufHeader { tensors: ModelTensor[]; }> { const versionBytes = await this.reader.take(4); - this.#little = - versionBytes.readUInt32LE() === 2 || versionBytes.readUInt32LE() === 3; - const version = this.#little - ? versionBytes.readUInt32LE() - : versionBytes.readUInt32BE(); - if (version !== 2 && version !== 3) + const littleVersion = versionBytes.readUInt32LE(); + const bigVersion = versionBytes.readUInt32BE(); + let version: number; + if (littleVersion === 2 || littleVersion === 3) { + this.#little = true; + version = littleVersion; + } else if (bigVersion === 2 || bigVersion === 3) { + this.#little = false; + version = bigVersion; + } else { throw new ModelFormatFailure( "unsupported", - `GGUF version ${String(version)} is unsupported; supported versions are 2 and 3`, + `GGUF version bytes ${versionBytes.toString("hex")} are unsupported (little-endian ${String(littleVersion)}, big-endian ${String(bigVersion)}; byte order unknown); supported versions are 2 and 3`, ); + } if (!this.#little && version !== 3) rejectModel("Big-endian GGUF requires version 3"); const tensorCount = await this.#unsigned(8); diff --git a/tests/boundary/model/modelArtifact.test.ts b/tests/boundary/model/modelArtifact.test.ts index 8cbb08ac07..dd7d9d8683 100644 --- a/tests/boundary/model/modelArtifact.test.ts +++ b/tests/boundary/model/modelArtifact.test.ts @@ -22,6 +22,57 @@ const providerAt = async (bytes: Buffer) => { return { provider, path, root }; }; +it.each([ + ["01000000", 1, 16_777_216], + ["00000001", 16_777_216, 1], + ["04000000", 4, 67_108_864], + ["00000004", 67_108_864, 4], + ["e7030000", 999, 3_875_733_504], + ["000003e7", 3_875_733_504, 999], + ["00000000", 0, 0], + ["ffffffff", 4_294_967_295, 4_294_967_295], +] as const)( + "preserves unsupported GGUF version bytes %s without assuming byte order", + async (versionBytes, little, big) => { + // No counts are needed to explain a version refusal. Do not reinterpret + // an unknown version's subsequent layout or publish a header observation. + const { provider, path } = await providerAt( + Buffer.from(`47475546${versionBytes}`, "hex"), + ); + const result = await provider.inspect({ path, hash: "header" }); + if (result.ok) throw new Error("Unknown version admitted"); + expect(result.error).toMatchObject({ + _tag: "AnalysisUnsupportedTargetError", + reason: `GGUF version bytes ${versionBytes} are unsupported (little-endian ${String(little)}, big-endian ${String(big)}; byte order unknown); supported versions are 2 and 3`, + }); + }, +); + +it("continues reading little-endian GGUF v2 declarations", async () => { + const { provider, path } = await providerAt( + ggufFixture({ version: 2 }).bytes, + ); + const result = await provider.inspect({ path, hash: "header" }); + if (!result.ok) throw result.error; + expect(result.value).toMatchObject({ version: 2, byte_order: "little" }); + expect(result.value.tensors.map(({ name }) => name)).toEqual([ + "weight", + "quant", + ]); +}); + +it("continues refusing the unsupported big-endian GGUF v2 layout", async () => { + const { provider, path } = await providerAt( + ggufFixture({ version: 2, bigEndian: true }).bytes, + ); + const result = await provider.inspect({ path, hash: "header" }); + if (result.ok) throw new Error("Big-endian GGUF v2 admitted"); + expect(result.error).toMatchObject({ + _tag: "BinaryTargetError", + constraint: "file_format_rejected", + }); +}); + it.each([false, true])( "reads exact GGUF declarations, endian=%s, without hashing tensor payload", async (bigEndian) => {