Skip to content

Commit 76e2e5a

Browse files
author
Feli
committed
Minimize change to main branch.
1 parent cbd8f66 commit 76e2e5a

3 files changed

Lines changed: 237 additions & 45 deletions

File tree

mlx/io/gguf.cpp

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ void set_mx_value_from_gguf(
132132
value = array(val->boolval, bool_);
133133
break;
134134
case GGUF_VALUE_TYPE_STRING:
135-
value =
136-
std::string(val->string.string, static_cast<int>(val->string.len));
135+
value = std::string(val->string.string, val->string.len);
137136
break;
138137
case GGUF_VALUE_TYPE_FLOAT64:
139138
value = array(val->float64, float32);
@@ -182,7 +181,7 @@ void set_mx_value_from_gguf(
182181
for (auto& str : strs) {
183182
auto str_val = reinterpret_cast<gguf_string*>(data);
184183
data += (str_val->len + sizeof(gguf_string));
185-
str = std::string(str_val->string, static_cast<int>(str_val->len));
184+
str = std::string(str_val->string, str_val->len);
186185
ctx->off += (str_val->len + sizeof(gguf_string));
187186
}
188187
value = std::move(strs);
@@ -208,21 +207,109 @@ void set_mx_value_from_gguf(
208207
}
209208
}
210209

210+
inline size_t gguf_value_type_size(uint32_t type) {
211+
switch (type) {
212+
case GGUF_VALUE_TYPE_BOOL:
213+
case GGUF_VALUE_TYPE_UINT8:
214+
case GGUF_VALUE_TYPE_INT8:
215+
return 1;
216+
case GGUF_VALUE_TYPE_UINT16:
217+
case GGUF_VALUE_TYPE_INT16:
218+
return 2;
219+
case GGUF_VALUE_TYPE_UINT32:
220+
case GGUF_VALUE_TYPE_INT32:
221+
case GGUF_VALUE_TYPE_FLOAT32:
222+
return 4;
223+
case GGUF_VALUE_TYPE_UINT64:
224+
case GGUF_VALUE_TYPE_INT64:
225+
case GGUF_VALUE_TYPE_FLOAT64:
226+
return 8;
227+
default:
228+
return 0;
229+
}
230+
}
231+
232+
void check_metadata_value_in_file(
233+
const gguf_ctx* ctx,
234+
uint32_t type,
235+
const gguf_value* val) {
236+
auto end = ctx->data + ctx->size;
237+
// Bytes available from a pointer up to the end of the mapping; 0 if the
238+
// pointer lies outside [ctx->data, end].
239+
auto avail = [&](const uint8_t* p) -> size_t {
240+
return (p < ctx->data || p > end) ? 0 : static_cast<size_t>(end - p);
241+
};
242+
auto base = reinterpret_cast<const uint8_t*>(val);
243+
auto fail = [](const char* what) {
244+
std::ostringstream msg;
245+
msg << "[load_gguf] " << what
246+
<< " Perhaps an incomplete download or corrupt file?";
247+
throw std::runtime_error(msg.str());
248+
};
249+
250+
size_t fixed = gguf_value_type_size(type);
251+
if (fixed) {
252+
if (fixed > avail(base)) {
253+
fail("Metadata value extends past the end of the file.");
254+
}
255+
return;
256+
}
257+
258+
auto check_string = [&](const uint8_t* p) -> const uint8_t* {
259+
uint64_t len = reinterpret_cast<const gguf_string*>(p)->len;
260+
if (sizeof(uint64_t) + len > avail(p)) {
261+
fail("String metadata value extends past the end of the file.");
262+
}
263+
return p + sizeof(uint64_t) + len;
264+
};
265+
266+
if (type == GGUF_VALUE_TYPE_STRING) {
267+
if (sizeof(uint64_t) > avail(base)) {
268+
fail("String metadata value extends past the end of the file.");
269+
}
270+
check_string(base);
271+
return;
272+
}
273+
274+
if (type == GGUF_VALUE_TYPE_ARRAY) {
275+
if (gguf_array_header_size > avail(base)) {
276+
fail("Metadata value extends past the end of the file.");
277+
}
278+
const uint8_t* elt = base + gguf_array_header_size;
279+
size_t elt_size = gguf_value_type_size(val->array.type);
280+
if (elt_size) {
281+
if (val->array.len > avail(elt) / elt_size) {
282+
fail("Array metadata value extends past the end of the file.");
283+
}
284+
return;
285+
}
286+
if (val->array.type == GGUF_VALUE_TYPE_STRING) {
287+
const uint8_t* p = elt;
288+
for (uint64_t i = 0; i < val->array.len; i++) {
289+
if (sizeof(uint64_t) > avail(p)) {
290+
fail("Array metadata value extends past the end of the file.");
291+
}
292+
p = check_string(p);
293+
}
294+
}
295+
return;
296+
}
297+
298+
throw std::runtime_error("[load_gguf] Received unexpected type.");
299+
}
300+
211301
std::unordered_map<std::string, GGUFMetaData> load_metadata(gguf_ctx* ctx) {
212302
std::unordered_map<std::string, GGUFMetaData> metadata;
213303
gguf_key key;
214304
while (gguf_get_key(ctx, &key)) {
305+
check_metadata_value_in_file(ctx, key.type, key.val);
215306
std::string key_name = std::string(key.name, key.namelen);
216307
auto& val = metadata.insert({key_name, GGUFMetaData{}}).first->second;
217308
set_mx_value_from_gguf(ctx, key.type, key.val, val);
218309
}
219310
return metadata;
220311
}
221312

222-
// gguflib computes weights_data as ctx->data + ctx->data_off + the tensor's
223-
// offset field in unsigned arithmetic, without comparing the result against the
224-
// mapping, so a crafted offset can point outside the file or -- if the addition
225-
// wraps -- back inside it at the wrong bytes.
226313
void check_tensor_in_file(const gguf_ctx* ctx, const gguf_tensor& tensor) {
227314
auto fail = [&tensor](const std::string& what) {
228315
std::ostringstream msg;

mlx/io/gguf_quants.cpp

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <cstdint>
44
#include <cstring>
5+
#include <numeric>
56

67
#include "mlx/io/gguf.h"
78

@@ -121,27 +122,28 @@ void gguf_load_quantized(
121122
if (shape[shape.size() - 1] % weights_per_block != 0) {
122123
std::ostringstream msg;
123124
msg << "[load_gguf] tensor " << name
124-
<< " has incompatible last dim shape: " << shape[shape.size() - 1];
125+
<< "has incompatible last dim shape: " << shape[shape.size() - 1];
125126
throw std::runtime_error(msg.str());
126127
}
127128

128129
auto weights_shape = shape;
129130
weights_shape.back() /= (weights_per_byte * 4);
130131

131-
auto checked_product = [&](const Shape& dims) {
132-
size_t product = 1;
133-
for (auto dim : dims) {
134-
if (__builtin_mul_overflow(product, static_cast<size_t>(dim), &product)) {
135-
std::ostringstream msg;
136-
msg << "[load_gguf] tensor " << name << " shape size overflow";
137-
throw std::runtime_error(msg.str());
138-
}
132+
auto checked_product = [&](size_t product, ShapeElem dim) {
133+
if (__builtin_mul_overflow(product, static_cast<size_t>(dim), &product)) {
134+
std::ostringstream msg;
135+
msg << "[load_gguf] tensor " << name << " shape size overflow";
136+
throw std::runtime_error(msg.str());
139137
}
140138
return product;
141139
};
142140

141+
const size_t weights_count = std::accumulate(
142+
weights_shape.begin(), weights_shape.end(), size_t{1}, checked_product);
143+
143144
shape[shape.size() - 1] = shape[shape.size() - 1] / weights_per_block;
144-
const size_t block_count = checked_product(shape);
145+
const size_t block_count =
146+
std::accumulate(shape.begin(), shape.end(), size_t{1}, checked_product);
145147

146148
const uint64_t bytes_per_block = tensor.type == GGUF_TYPE_Q4_0
147149
? 18
@@ -158,45 +160,35 @@ void gguf_load_quantized(
158160
throw std::runtime_error(msg.str());
159161
}
160162

161-
const size_t weights_bytes_per_block =
162-
tensor.type == GGUF_TYPE_Q8_0 ? 32 : 16;
163163
size_t w_nbytes;
164-
if (__builtin_mul_overflow(block_count, weights_bytes_per_block, &w_nbytes)) {
164+
if (__builtin_mul_overflow(uint32.size(), weights_count, &w_nbytes)) {
165165
std::ostringstream msg;
166166
msg << "[load_gguf] tensor " << name << " weights size overflow";
167167
throw std::runtime_error(msg.str());
168168
}
169169

170-
auto weights_buffer = allocator::malloc(w_nbytes);
171-
if (!weights_buffer.raw_ptr()) {
172-
std::ostringstream msg;
173-
msg << "[load_gguf] tensor " << name << " allocation failed";
174-
throw std::runtime_error(msg.str());
175-
}
176-
array weights(weights_buffer, std::move(weights_shape), uint32);
170+
auto checked_malloc = [&](size_t size, const char* what) {
171+
auto buffer = allocator::malloc(size);
172+
if (!buffer.raw_ptr()) {
173+
std::ostringstream msg;
174+
msg << "[load_gguf] tensor " << name << " " << what
175+
<< " allocation failed";
176+
throw std::runtime_error(msg.str());
177+
}
178+
return buffer;
179+
};
180+
181+
array weights(
182+
checked_malloc(w_nbytes, "weights"), std::move(weights_shape), uint32);
177183

178184
size_t sb_nbytes;
179185
if (__builtin_mul_overflow(float16.size(), block_count, &sb_nbytes)) {
180186
std::ostringstream msg;
181187
msg << "[load_gguf] tensor " << name << " scales/biases size overflow";
182188
throw std::runtime_error(msg.str());
183189
}
184-
185-
auto scales_buffer = allocator::malloc(sb_nbytes);
186-
if (!scales_buffer.raw_ptr()) {
187-
std::ostringstream msg;
188-
msg << "[load_gguf] tensor " << name << " scales/biases allocation failed";
189-
throw std::runtime_error(msg.str());
190-
}
191-
array scales(scales_buffer, shape, float16);
192-
193-
auto biases_buffer = allocator::malloc(sb_nbytes);
194-
if (!biases_buffer.raw_ptr()) {
195-
std::ostringstream msg;
196-
msg << "[load_gguf] tensor " << name << " biases allocation failed";
197-
throw std::runtime_error(msg.str());
198-
}
199-
array biases(biases_buffer, std::move(shape), float16);
190+
array scales(checked_malloc(sb_nbytes, "scales"), shape, float16);
191+
array biases(checked_malloc(sb_nbytes, "biases"), std::move(shape), float16);
200192

201193
if (tensor.type == GGUF_TYPE_Q4_0) {
202194
extract_q4_0_data(tensor, weights, scales, biases);

tests/load_tests.cpp

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright © 2023 Apple Inc.
22

3-
#include <cstdint>
43
#include <filesystem>
54
#include <fstream>
65
#include <stdexcept>
@@ -258,6 +257,120 @@ TEST_CASE("test gguf tensor data offset validation") {
258257
}
259258
}
260259

260+
// Writes a metadata-only GGUF (no tensors) whose metadata KV section is
261+
// `kv_section` verbatim, so a caller can encode values whose lengths exceed the
262+
// file to exercise check_metadata_value_in_file(). `kv_count` must match the
263+
// number of KV pairs encoded in `kv_section`.
264+
void write_raw_gguf_metadata(
265+
const std::string& path,
266+
uint64_t kv_count,
267+
const std::vector<char>& kv_section) {
268+
std::ofstream out(path, std::ios::binary);
269+
auto u32 = [&out](uint32_t v) {
270+
out.write(reinterpret_cast<const char*>(&v), 4);
271+
};
272+
auto u64 = [&out](uint64_t v) {
273+
out.write(reinterpret_cast<const char*>(&v), 8);
274+
};
275+
out.write("GGUF", 4);
276+
u32(3); // version
277+
u64(0); // tensor_count
278+
u64(kv_count); // metadata_kv_count
279+
out.write(kv_section.data(), kv_section.size());
280+
}
281+
282+
TEST_CASE("test gguf metadata value validation") {
283+
// A STRING/ARRAY metadata value claiming a length larger than the file must
284+
// be rejected rather than read past the end of the mapping. See PR #4212.
285+
286+
auto append_string_kv = [](std::vector<char>& b,
287+
const std::string& key,
288+
uint64_t claimed_len,
289+
bool write_payload) {
290+
auto put = [&](const void* p, size_t n) {
291+
b.insert(
292+
b.end(),
293+
static_cast<const char*>(p),
294+
static_cast<const char*>(p) + n);
295+
};
296+
uint64_t klen = key.size();
297+
put(&klen, 8);
298+
put(key.data(), key.size());
299+
uint32_t vt = 8; // GGUF_VALUE_TYPE_STRING
300+
put(&vt, 4);
301+
put(&claimed_len, 8);
302+
if (write_payload) {
303+
b.insert(b.end(), claimed_len, '\0');
304+
}
305+
};
306+
307+
auto append_array_kv = [](std::vector<char>& b,
308+
const std::string& key,
309+
uint32_t elt_type,
310+
uint64_t claimed_len) {
311+
auto put = [&](const void* p, size_t n) {
312+
b.insert(
313+
b.end(),
314+
static_cast<const char*>(p),
315+
static_cast<const char*>(p) + n);
316+
};
317+
uint64_t klen = key.size();
318+
put(&klen, 8);
319+
put(key.data(), key.size());
320+
uint32_t vt = 9; // GGUF_VALUE_TYPE_ARRAY
321+
put(&vt, 4);
322+
put(&elt_type, 4);
323+
put(&claimed_len, 8);
324+
};
325+
326+
SUBCASE("valid empty and small strings load") {
327+
std::vector<char> kv;
328+
append_string_kv(kv, "empty", 0, false);
329+
append_string_kv(kv, "small", 5, true);
330+
std::string file_path = get_temp_file("test_gguf_meta_ok.gguf");
331+
write_raw_gguf_metadata(file_path, 2, kv);
332+
auto [weights, metadata] = load_gguf(file_path);
333+
CHECK(weights.empty());
334+
CHECK(std::get<std::string>(metadata.at("empty")) == "");
335+
CHECK(std::get<std::string>(metadata.at("small")) == std::string(5, '\0'));
336+
}
337+
338+
SUBCASE("string length extends past the end of the file") {
339+
// Claims 100 bytes of payload, none of which are present.
340+
std::vector<char> kv;
341+
append_string_kv(kv, "s", 100, false);
342+
std::string file_path = get_temp_file("test_gguf_meta_str_past.gguf");
343+
write_raw_gguf_metadata(file_path, 1, kv);
344+
CHECK_THROWS_AS(load_gguf(file_path), std::runtime_error);
345+
}
346+
347+
SUBCASE("string length far past the end of the file") {
348+
std::vector<char> kv;
349+
append_string_kv(kv, "s", 1ull << 40, false);
350+
std::string file_path = get_temp_file("test_gguf_meta_str_far.gguf");
351+
write_raw_gguf_metadata(file_path, 1, kv);
352+
CHECK_THROWS_AS(load_gguf(file_path), std::runtime_error);
353+
}
354+
355+
SUBCASE("fixed-size array length extends past the end of the file") {
356+
// GGUF_VALUE_TYPE_UINT8 = 0; claims 2^40 elements, none present.
357+
std::vector<char> kv;
358+
append_array_kv(kv, "a", 0, 1ull << 40);
359+
std::string file_path = get_temp_file("test_gguf_meta_arr_past.gguf");
360+
write_raw_gguf_metadata(file_path, 1, kv);
361+
CHECK_THROWS_AS(load_gguf(file_path), std::runtime_error);
362+
}
363+
364+
SUBCASE("string array element length extends past the end of the file") {
365+
// GGUF_VALUE_TYPE_STRING = 8; two elements, neither present.
366+
std::vector<char> kv;
367+
append_array_kv(kv, "a", 8, 2);
368+
std::string file_path = get_temp_file("test_gguf_meta_strarr_past.gguf");
369+
write_raw_gguf_metadata(file_path, 1, kv);
370+
CHECK_THROWS_AS(load_gguf(file_path), std::runtime_error);
371+
}
372+
}
373+
261374
TEST_CASE("test gguf metadata") {
262375
std::string file_path = get_temp_file("test_arr.gguf");
263376
using dict = std::unordered_map<std::string, array>;
@@ -510,7 +623,7 @@ TEST_CASE("test gguf quantized tensor security") {
510623

511624
SUBCASE("dimension exceeding int32 rejected") {
512625
std::string file_path = get_temp_file("test_gguf_qbigdim.gguf");
513-
uint64_t big_dim = static_cast<uint64_t>(INT32_MAX) + 1;
626+
uint64_t big_dim = 1ull << 31;
514627
write_raw_quantized_gguf(file_path, {big_dim}, 8 /* Q8_0 */, big_dim);
515628
CHECK_THROWS_AS(load_gguf(file_path), std::runtime_error);
516629
}

0 commit comments

Comments
 (0)