Skip to content

Cross-check tensor data size against shape in FP8/FP4 dequant (OOB read)#543

Open
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix/dequant-shape-offsets
Open

Cross-check tensor data size against shape in FP8/FP4 dequant (OOB read)#543
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix/dequant-shape-offsets

Conversation

@gigioneggiando

Copy link
Copy Markdown

Summary

A safetensors tensor's shape and its data_offsets byte range are independent header fields. db_read() sizes the on-disk buffer (st_value.nbytes) from data_offsets, while dequant_fp8_weight() / dequant_fp4_weight() index w->data and scale->data by shape. The two are never cross-validated.

So a weight that declares a large shape but a tiny data_offsets range — e.g. shape=[128,128] (16384 elements) with only 128 bytes of data — makes the dequant loops read far past the end of the heap buffer: a heap-buffer-overflow read, which leaks adjacent heap bytes into the produced GGUF.

Fix

Before the dequant loops, require nbytes to cover the shape-driven indexing (one byte per element for the F8_E4M3 / I8 weights and F8_E8M0 scales). Mismatched tensors are rejected with a clear error instead of reading OOB.

Verification

Drove a real crafted safetensors shard + index through db_open → db_read → dequant_fp8_weight under ASan:

  • before: AddressSanitizer: heap-buffer-overflow READ at dequant_fp8_weight off the 128-byte buffer.
  • after: error: FP8 tensor data smaller than its declared shape, no sanitizer error.

Found during a coordinated security review of ds4; a standalone offline PoC is available on request.

A safetensors tensor's shape and its data_offsets byte range are
independent header fields. db_read() sizes the buffer (st_value.nbytes)
from data_offsets, while dequant_fp8_weight() / dequant_fp4_weight() index
w->data and scale->data by shape. They were never cross-validated, so a
weight that declares a large shape but a tiny data_offsets range (e.g.
shape [128,128] = 16384 elements with only 128 bytes of data) makes the
loops read past the end of the heap buffer -- a heap-buffer-overflow read,
leaking adjacent heap bytes into the produced GGUF.

Before the loops, require nbytes to cover the shape-driven access: one byte
per element for the F8_E4M3 / I8 weights and F8_E8M0 scales.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant