tenseleyflow/documentlanguagemodel / d026bc9

Browse files

test: fuzz GGUF parser with hypothesis random bytes (audit-04 T6)

Authored by espadonne
SHA
d026bc9cd42016ebf279778f5ad37fd4d440ae59
Parents
6f61e7f
Tree
b30ea89

1 changed file

StatusFile+-
M tests/unit/test_properties.py 40 2
tests/unit/test_properties.pymodified
@@ -9,14 +9,16 @@ budget is tuned small.
99
 from __future__ import annotations
1010
 
1111
 import string
12
+from pathlib import Path
1213
 
1314
 import pytest
14
-from hypothesis import given
15
+from hypothesis import HealthCheck, given, settings
1516
 from hypothesis import strategies as st
1617
 
17
-from dlm.export.errors import UnsafeMergeError
18
+from dlm.export.errors import PreflightError, UnsafeMergeError
1819
 from dlm.export.merge import check_merge_safety
1920
 from dlm.export.plan import ExportPlan
21
+from dlm.export.tokenizer_sync import read_gguf_vocab_size
2022
 from dlm.io.ulid import mint_ulid
2123
 from dlm.pack.integrity import rollup_sha256
2224
 
@@ -107,3 +109,39 @@ class TestMergeSafetyTruthTable:
107109
                 check_merge_safety(plan, was_qlora=was_qlora)
108110
         else:
109111
             check_merge_safety(plan, was_qlora=was_qlora)  # no raise
112
+
113
+
114
+# --- GGUF parser fuzz ---------------------------------------------------------
115
+
116
+
117
+class TestGgufParserFuzz:
118
+    """Feed random bytes at `read_gguf_vocab_size`; it must surface a typed
119
+    `PreflightError`, never leak `struct.error` / `MemoryError` / a raw
120
+    decoded string. Audit-04 T6 / B7 defense-in-depth.
121
+
122
+    Reusing `tmp_path` across hypothesis iterations is deliberate — we
123
+    overwrite the file each run, and spinning up a fresh dir per-sample
124
+    would dominate the test runtime.
125
+    """
126
+
127
+    @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50)
128
+    @given(
129
+        payload=st.binary(min_size=0, max_size=256).filter(
130
+            # Skip valid headers — fuzzing valid packets isn't the point.
131
+            lambda b: not b.startswith(b"GGUF")
132
+        )
133
+    )
134
+    def test_random_bytes_raise_preflight_error(self, payload: bytes, tmp_path: Path) -> None:
135
+        path = tmp_path / "fuzz.gguf"
136
+        path.write_bytes(payload)
137
+        with pytest.raises(PreflightError):
138
+            read_gguf_vocab_size(path)
139
+
140
+    @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=50)
141
+    @given(body=st.binary(min_size=0, max_size=256))
142
+    def test_random_body_after_magic_doesnt_crash(self, body: bytes, tmp_path: Path) -> None:
143
+        """Even with valid magic, garbage body → typed error, no crash."""
144
+        path = tmp_path / "fuzz.gguf"
145
+        path.write_bytes(b"GGUF" + body)
146
+        with pytest.raises(PreflightError):
147
+            read_gguf_vocab_size(path)