From b3dc2d28416102a543bb11f242992e19268fa72a Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 23 Oct 2026 02:49:07 -0500 Subject: [PATCH 1/2] Make tests/fuzz/Makefile parse and build every target again 575371d, which added the DiskANN targets, dropped the line continuation after ivf_rescore in FUZZ_TARGETS and the ivf_rescore rule's recipe, so make stops at the first DiskANN name and builds nothing: $ make -C tests/fuzz all Makefile:158: *** missing separator (did you mean TAB instead of 8 spaces?). Stop. 0de765f then removed numpy.c, which the numpy target still compiles. The list continues past ivf_rescore again, ivf_rescore has its recipe, and the numpy target and its dictionary are gone. make -C tests/fuzz all builds all 36 targets. --- tests/fuzz/Makefile | 9 ++++----- tests/fuzz/numpy.dict | 7 ------- 2 files changed, 4 insertions(+), 12 deletions(-) delete mode 100644 tests/fuzz/numpy.dict diff --git a/tests/fuzz/Makefile b/tests/fuzz/Makefile index 202dc2b7..9c500a7f 100644 --- a/tests/fuzz/Makefile +++ b/tests/fuzz/Makefile @@ -44,9 +44,6 @@ $(TARGET_DIR)/exec: exec.c $(FUZZ_SRCS) | $(TARGET_DIR) $(TARGET_DIR)/json: json.c $(FUZZ_SRCS) | $(TARGET_DIR) $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ -$(TARGET_DIR)/numpy: numpy.c $(FUZZ_SRCS) | $(TARGET_DIR) - $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ - # New targets $(TARGET_DIR)/shadow_corrupt: shadow-corrupt.c $(FUZZ_SRCS) | $(TARGET_DIR) $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ @@ -115,6 +112,8 @@ $(TARGET_DIR)/ivf_cell_overflow: ivf-cell-overflow.c $(FUZZ_SRCS) | $(TARGET_DIR $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ $(TARGET_DIR)/ivf_rescore: ivf-rescore.c $(FUZZ_SRCS) | $(TARGET_DIR) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ + $(TARGET_DIR)/diskann_operations: diskann-operations.c $(FUZZ_SRCS) | $(TARGET_DIR) $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ @@ -145,7 +144,7 @@ $(TARGET_DIR)/diskann_prune_direct: diskann-prune-direct.c $(FUZZ_SRCS) | $(TARG $(TARGET_DIR)/diskann_command_inject: diskann-command-inject.c $(FUZZ_SRCS) | $(TARGET_DIR) $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SRCS) $< -o $@ -FUZZ_TARGETS = vec0_create exec json numpy \ +FUZZ_TARGETS = vec0_create exec json \ shadow_corrupt vec0_operations scalar_functions \ vec0_create_full metadata_columns vec_each vec_mismatch \ vec0_delete_completeness \ @@ -154,7 +153,7 @@ FUZZ_TARGETS = vec0_create exec json numpy \ rescore_quantize_edge rescore_interleave \ ivf_create ivf_operations \ ivf_quantize ivf_kmeans ivf_shadow_corrupt \ - ivf_knn_deep ivf_cell_overflow ivf_rescore + ivf_knn_deep ivf_cell_overflow ivf_rescore \ diskann_operations diskann_create diskann_graph_corrupt \ diskann_deep_search diskann_blob_truncate \ diskann_delete_stress diskann_buffer_flush \ diff --git a/tests/fuzz/numpy.dict b/tests/fuzz/numpy.dict deleted file mode 100644 index f91b82f1..00000000 --- a/tests/fuzz/numpy.dict +++ /dev/null @@ -1,7 +0,0 @@ -magic="\x93NUMPY" -lparen="(" -rparen=")" -lbrace="{" -rbrace="}" -sq1="\"" -sq2="'" From dc77578b9f2efc75e1b4a578bf8ca31e1bc3ad16 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 23 Oct 2026 02:49:07 -0500 Subject: [PATCH 2/2] Check rescore_quantize_edge against the [-1, 1] int8 quantizer 575371d replaced rescore's min/max int8 quantizer with a fixed map of [-1, 1] onto [-128, 127], and mode 3 still asserted that a vector of one repeated value quantizes to all zeros, as the replaced quantizer did for a zero range. The current quantizer gives 0 only for values near 0.004, so the target aborts within a few hundred inputs (a vector of NaN gives 127). Mode 3 now asserts that every element gets the same code, which holds for any element-wise quantizer, and the comments on modes 2 and 4 describe the clamps the current quantizer has. Modes 1 and 2 read the fuzz bytes as floats from one byte past the mode byte, a misaligned load UBSan reports in rescore_quantize_float_to_int8(); they copy the bytes to a float array first. The target runs 3.7 million inputs in 60 seconds without a report. --- tests/fuzz/rescore-quantize-edge.c | 48 +++++++++++++++++------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/tests/fuzz/rescore-quantize-edge.c b/tests/fuzz/rescore-quantize-edge.c index 4ab9e207..0ac96772 100644 --- a/tests/fuzz/rescore-quantize-edge.c +++ b/tests/fuzz/rescore-quantize-edge.c @@ -23,9 +23,11 @@ extern size_t _test_rescore_quantized_byte_size_int8(size_t dimensions); * - Tests rescore_quantized_byte_size with arbitrary dimension values * (including 0, 1, 7, MAX values -- looking for integer division issues) * - Passes raw float reinterpretation of fuzz bytes (NaN, Inf, denormals, - * negative zero -- these are the values that break min/max/range logic) - * - Tests the int8 quantizer with all-identical values (range=0 branch) - * - Tests the int8 quantizer with extreme ranges (overflow in scale calc) + * negative zero: the values that must reach the int8 quantizer's clamps + * before its conversion to int8) + * - Tests the int8 quantizer with all-identical values (each element is + * quantized on its own, so all get the same code) + * - Tests the int8 quantizer with values far outside [-1, 1] * - Tests bit quantizer with exact float threshold (0.0f boundary) */ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { @@ -63,10 +65,13 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { size_t dim = (num_floats / 8) * 8; if (dim == 0) return 0; - const float *src = (const float *)data; + /* The fuzz bytes start one byte past the mode byte, so they are copied + out to be read as floats at float alignment. */ + float *src = (float *)malloc(dim * sizeof(float)); size_t bit_bytes = dim / 8; uint8_t *dst = (uint8_t *)malloc(bit_bytes); - if (!dst) return 0; + if (!src || !dst) { free(src); free(dst); return 0; } + memcpy(src, data, dim * sizeof(float)); _test_rescore_quantize_float_to_bit(src, dst, dim); @@ -82,23 +87,25 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { /* NaN: comparison is false, so bit_set should be 0 */ } + free(src); free(dst); break; } case 2: { - /* Int8 quantize with raw reinterpreted floats. - The dangerous paths: - - All values identical (range == 0) -> memset path - - vmin/vmax with NaN (NaN < anything is false, NaN > anything is false) - - Extreme range causing scale = 255/range to be Inf or 0 - - denormals near the clamping boundaries */ + /* Int8 quantize with raw reinterpreted floats. The quantizer maps + [-1, 1] onto [-128, 127] and clamps before converting to int8, so + NaN, Inf and values far outside [-1, 1] must each reach a clamp: + converting a float outside int8's range to int8 is undefined + behavior, which UBSan reports. */ size_t num_floats = size / sizeof(float); if (num_floats == 0) return 0; - const float *src = (const float *)data; + /* Copied out to float alignment, as in mode 1 */ + float *src = (float *)malloc(num_floats * sizeof(float)); int8_t *dst = (int8_t *)malloc(num_floats); - if (!dst) return 0; + if (!src || !dst) { free(src); free(dst); return 0; } + memcpy(src, data, num_floats * sizeof(float)); _test_rescore_quantize_float_to_int8(src, dst, num_floats); @@ -108,12 +115,13 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { assert(dst[i] >= -128 && dst[i] <= 127); } + free(src); free(dst); break; } case 3: { - /* Int8 quantize stress: all-same values (range=0 branch) */ + /* Int8 quantize stress: all-same values */ size_t dim = (size < 64) ? size : 64; if (dim == 0) return 0; @@ -128,9 +136,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { _test_rescore_quantize_float_to_int8(src, dst, dim); - /* All outputs should be 0 when range == 0 */ - for (size_t i = 0; i < dim; i++) { - assert(dst[i] == 0); + /* Each element is quantized on its own, so all get the same code */ + for (size_t i = 1; i < dim; i++) { + assert(dst[i] == dst[0]); } free(src); @@ -139,9 +147,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { } case 4: { - /* Int8 quantize with extreme range: one huge positive, one huge negative. - Tests scale = 255.0f / range overflow to Inf, then v * Inf = Inf, - then clamping to [-128, 127]. */ + /* Int8 quantize with extreme values: one huge positive, one huge + negative, and the rest thousands, all far outside [-1, 1] and + clamped to [-128, 127]. */ if (size < 2 * sizeof(float)) return 0; float extreme[2];