From e828ae6221abc4fbfb930824ac572972c5e6b9b1 Mon Sep 17 00:00:00 2001 From: FuturMix Date: Sat, 30 May 2026 00:30:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20gguf:=20fix=20GGML=5FPA?= =?UTF-8?q?D=20integer=20overflow=20via=20crafted=20general.alignment=20(C?= =?UTF-8?q?WE-190)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JavaScript bitwise operators (`&`, `~`) truncate to 32-bit signed integers. When `general.alignment >= 2^31`, `GGML_PAD(offset, alignment)` overflows, producing negative values that corrupt `tensorDataOffset` and cause `RangeError` in serialization paths. This bypasses the validation added in PR #2028 because values like `alignment = 2147483648` pass the positive-integer check but overflow in bitwise operations. Fix: - Add power-of-2 validation (matching llama.cpp `gguf.cpp` line 612) - Add upper bound `MAX_ALIGNMENT = 2^30` (largest safe power-of-2 for JS bitwise ops) - Validate in all three entry points: `gguf()`, `serializeGgufMetadata()`, and `buildGgufHeader()` Co-Authored-By: Claude Opus 4.6 --- packages/gguf/src/gguf.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/gguf/src/gguf.ts b/packages/gguf/src/gguf.ts index b91658e4d0..1a17aa25e4 100644 --- a/packages/gguf/src/gguf.ts +++ b/packages/gguf/src/gguf.ts @@ -40,6 +40,7 @@ const MAX_STRING_LENGTH = 10_000_000; // 10MB per string (CWE-770) const MAX_TENSOR_NDIMS = 8; // GGML supports up to 4, be generous (CWE-770) const MAX_ARRAY_RECURSION_DEPTH = 4; // nested ARRAY-of-ARRAY depth limit (CWE-674) const MAX_CHUNK_FETCHES_PER_VALUE = 30; // prevent infinite fetch loop (CWE-835) +const MAX_ALIGNMENT = 1 << 30; // 2^30 — largest power-of-2 safe for JS bitwise ops (CWE-190) const GGML_PAD = (x: number, n: number) => (x + n - 1) & ~(n - 1); // defined in ggml.h const PARALLEL_DOWNLOADS = 20; @@ -505,6 +506,12 @@ export async function gguf( if (alignment <= 0 || !Number.isInteger(alignment)) { throw new Error(`general.alignment must be a positive integer, got ${rawAlignment}`); } + if ((alignment & (alignment - 1)) !== 0) { + throw new Error(`general.alignment must be a power of 2, got ${rawAlignment}`); + } + if (alignment > MAX_ALIGNMENT) { + throw new Error(`general.alignment ${rawAlignment} exceeds maximum safe value (${MAX_ALIGNMENT}) for JS bitwise operations`); + } const tensorInfoEndBeforePadOffset = offset; const tensorDataOffset = BigInt(GGML_PAD(offset, alignment)); @@ -654,6 +661,9 @@ export function serializeGgufMetadata( ): Uint8Array { const littleEndian = options.littleEndian ?? true; const alignment = options.alignment ?? GGUF_DEFAULT_ALIGNMENT; + if (alignment <= 0 || !Number.isInteger(alignment) || (alignment & (alignment - 1)) !== 0 || alignment > MAX_ALIGNMENT) { + throw new Error(`alignment must be a power of 2 in [1, ${MAX_ALIGNMENT}], got ${alignment}`); + } const version = typedMetadata.version.value; // Start with GGUF magic number: "GGUF" @@ -762,6 +772,9 @@ export async function buildGgufHeader( }, ): Promise { const alignment = options.alignment ?? GGUF_DEFAULT_ALIGNMENT; + if (alignment <= 0 || !Number.isInteger(alignment) || (alignment & (alignment - 1)) !== 0 || alignment > MAX_ALIGNMENT) { + throw new Error(`alignment must be a power of 2 in [1, ${MAX_ALIGNMENT}], got ${alignment}`); + } const version = updatedMetadata.version.value; // Serialize the new metadata