Skip to content

Commit b669801

Browse files
asg017claude
andcommitted
Add UBSAN findings TODO and improve vec-mismatch fuzzer
Document three classes of undefined behavior found by UBSAN: function pointer type mismatches, misaligned f32 reads, and float-to-integer overflow in vec_quantize_int8. Improve vec-mismatch fuzzer to cover all error-path cleanup patterns: type mismatches, dimension mismatches, single-arg functions, and both text and blob inputs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ce1ef3 commit b669801

9 files changed

Lines changed: 195 additions & 95 deletions

File tree

tests/fuzz/TODO.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Fuzz Testing TODO: Undefined Behavior Findings
2+
3+
UBSAN findings from fuzz targets. None are crash-level bugs, but all are
4+
formally undefined behavior per the C standard.
5+
6+
## Class 1: Function pointer type mismatch (~20 sites)
7+
8+
`fvec_cleanup_noop` is defined as `void (f32 *)` but called through
9+
`vector_cleanup` which is `void (*)(void *)`. Two cleanup typedefs exist
10+
with incompatible signatures:
11+
12+
```c
13+
typedef void (*vector_cleanup)(void *p); // line 597
14+
typedef void (*fvec_cleanup)(f32 *vector); // line 695
15+
```
16+
17+
Affected lines: 1031, 1049, 1050, 1160, 1200, 1201, 1241, 1242, 1282,
18+
1283, 1324, 1325, 1356, 1424, 1524, 1525, 1582, 1583, 1699, 1749, 1798,
19+
2520, 7236, 8501, and sqlite3.c:82930 (via sqlite3_result_blob destructor).
20+
21+
Low practical severity — calling conventions on all real platforms pass
22+
`f32 *` and `void *` identically — but flags on every UBSAN run.
23+
24+
Fix: change `fvec_cleanup_noop` to take `void *`, or unify the typedefs.
25+
26+
## Class 2: Misaligned f32 reads (~10 sites)
27+
28+
`f32` (4-byte alignment required) read from potentially unaligned addresses.
29+
Happens when a blob from SQLite's internal storage is cast to `f32 *` and
30+
dereferenced. The blob pointer may not be 4-byte aligned.
31+
32+
Affected lines: 369, 446, 473-475, 1401, 1461, 1501, 1559, 1653, 1726,
33+
1789, 1793.
34+
35+
Medium severity — silent on x86/ARM64 (hardware supports unaligned float
36+
access) but UB on strict-alignment architectures.
37+
38+
Fix: use `memcpy` to load floats from potentially-unaligned memory, or
39+
ensure blob pointers are aligned before use.
40+
41+
## Class 3: Float-to-integer overflow (1 site)
42+
43+
`vec_quantize_int8` at line 1461 — when `srcVector[i]` is a large float,
44+
the expression `((srcVector[i] - (-1.0)) / step) - 128` overflows
45+
`signed char` range. Assigning this to `i8 out[i]` is UB.
46+
47+
Low-medium severity — silent truncation in practice.
48+
49+
Fix: clamp the result before cast.
3 Bytes
Binary file not shown.
6 Bytes
Binary file not shown.
4 Bytes
Binary file not shown.
11 Bytes
Binary file not shown.
13 Bytes
Binary file not shown.
3 Bytes
Binary file not shown.
3 Bytes
Binary file not shown.

tests/fuzz/vec-mismatch.c

Lines changed: 146 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,119 +8,170 @@
88
#include <assert.h>
99

1010
/*
11-
* Fuzz target for two-argument vector functions (vec_distance_*, vec_add,
12-
* vec_sub) where the first argument is always a valid vector and the second
13-
* is fuzz-derived. This exercises the ensure_vector_match() error paths
14-
* where the first vector parses successfully but the second does not.
11+
* Fuzz target that exercises error-path cleanup in vector functions.
1512
*
16-
* Critical coverage: when arg1 is TEXT (JSON-parsed), the cleanup function
17-
* is sqlite3_free rather than a no-op, so cleanup bugs become observable.
13+
* The key insight: when a vector is parsed from JSON TEXT, the cleanup
14+
* function is sqlite3_free (heap allocator). When parsed from BLOB,
15+
* cleanup is a no-op. Bugs in cleanup code (wrong pointer, missing
16+
* cleanup, double-free) are only observable with the sqlite3_free path.
1817
*
19-
* The first byte selects the function. The remaining bytes form arg 2.
18+
* This fuzzer systematically covers:
19+
* 1. Valid JSON arg1 + invalid fuzz arg2 (parse failure → cleanup arg1)
20+
* 2. Valid JSON arg1 + valid JSON arg2 with different dimensions
21+
* (dimension mismatch → cleanup both)
22+
* 3. Valid JSON arg1 + int8/bit blob arg2 with mismatched type
23+
* (type mismatch → cleanup both)
24+
* 4. Fuzz arg1 + valid JSON arg2 (parse failure of arg1, no cleanup)
25+
* 5. Single-arg functions with JSON text (normal cleanup path)
26+
* 6. Single-arg functions with fuzz text (parse failure path)
2027
*/
2128

29+
/* Helper: bind a valid vector to a statement parameter.
30+
* mode selects the vector type and format. */
31+
static void bind_valid_vector(sqlite3_stmt *stmt, int param, int mode) {
32+
/* JSON text vectors — cleanup = sqlite3_free */
33+
static const char *json_f32_4d = "[1.0, 0.0, 0.0, 0.0]";
34+
static const char *json_f32_2d = "[1.0, 2.0]";
35+
static const char *json_f32_1d = "[1.0]";
36+
37+
/* Blob vectors — cleanup = noop */
38+
static const float blob_f32_4d[] = {1.0f, 0.0f, 0.0f, 0.0f};
39+
static const float blob_f32_2d[] = {1.0f, 2.0f};
40+
41+
/* int8 blob — 4 bytes = 4 dimensions */
42+
static const int8_t blob_int8_4d[] = {10, 20, 30, 40};
43+
44+
/* bit blob — 1 byte = 8 bits */
45+
static const uint8_t blob_bit_1b[] = {0xAA};
46+
47+
switch (mode % 7) {
48+
case 0: sqlite3_bind_text(stmt, param, json_f32_4d, -1, SQLITE_STATIC); break;
49+
case 1: sqlite3_bind_text(stmt, param, json_f32_2d, -1, SQLITE_STATIC); break;
50+
case 2: sqlite3_bind_text(stmt, param, json_f32_1d, -1, SQLITE_STATIC); break;
51+
case 3: sqlite3_bind_blob(stmt, param, blob_f32_4d, sizeof(blob_f32_4d), SQLITE_STATIC); break;
52+
case 4: sqlite3_bind_blob(stmt, param, blob_f32_2d, sizeof(blob_f32_2d), SQLITE_STATIC); break;
53+
case 5: /* int8 — must set subtype */
54+
sqlite3_bind_blob(stmt, param, blob_int8_4d, sizeof(blob_int8_4d), SQLITE_STATIC);
55+
break;
56+
case 6: /* bit — must set subtype */
57+
sqlite3_bind_blob(stmt, param, blob_bit_1b, sizeof(blob_bit_1b), SQLITE_STATIC);
58+
break;
59+
}
60+
}
61+
62+
static void run_query(sqlite3 *db, const char *sql,
63+
int arg1_mode, int arg2_mode,
64+
const uint8_t *fuzz, int fuzz_len,
65+
int fuzz_arg, int fuzz_as_text) {
66+
sqlite3_stmt *stmt = NULL;
67+
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
68+
if (rc != SQLITE_OK) return;
69+
70+
int nParams = sqlite3_bind_parameter_count(stmt);
71+
72+
for (int p = 1; p <= nParams; p++) {
73+
if (p == fuzz_arg) {
74+
/* Bind fuzz data */
75+
if (fuzz_as_text)
76+
sqlite3_bind_text(stmt, p, (const char *)fuzz, fuzz_len, SQLITE_STATIC);
77+
else
78+
sqlite3_bind_blob(stmt, p, fuzz, fuzz_len, SQLITE_STATIC);
79+
} else if (p == 1) {
80+
bind_valid_vector(stmt, p, arg1_mode);
81+
} else {
82+
bind_valid_vector(stmt, p, arg2_mode);
83+
}
84+
}
85+
86+
sqlite3_step(stmt);
87+
sqlite3_finalize(stmt);
88+
}
89+
2290
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
23-
if (size < 2) return 0;
91+
if (size < 3) return 0;
2492

2593
int rc;
2694
sqlite3 *db;
27-
sqlite3_stmt *stmt = NULL;
2895

2996
rc = sqlite3_open(":memory:", &db);
3097
assert(rc == SQLITE_OK);
3198
rc = sqlite3_vec_init(db, NULL, NULL);
3299
assert(rc == SQLITE_OK);
33100

101+
/* --- Decode fuzz control bytes --- */
102+
uint8_t b0 = data[0];
103+
uint8_t b1 = data[1];
104+
uint8_t b2 = data[2];
105+
const uint8_t *payload = data + 3;
106+
int payload_size = (int)(size - 3);
107+
34108
/* Two-argument vector functions */
35-
static const char *queries[] = {
36-
"SELECT vec_distance_l2(?, ?)", /* 0 */
37-
"SELECT vec_distance_cosine(?, ?)", /* 1 */
38-
"SELECT vec_distance_l1(?, ?)", /* 2 */
39-
"SELECT vec_distance_hamming(?, ?)", /* 3 */
40-
"SELECT vec_add(?, ?)", /* 4 */
41-
"SELECT vec_sub(?, ?)", /* 5 */
109+
static const char *two_arg[] = {
110+
"SELECT vec_distance_l2(?, ?)",
111+
"SELECT vec_distance_cosine(?, ?)",
112+
"SELECT vec_distance_l1(?, ?)",
113+
"SELECT vec_distance_hamming(?, ?)",
114+
"SELECT vec_add(?, ?)",
115+
"SELECT vec_sub(?, ?)",
42116
};
43-
static const int nQueries = sizeof(queries) / sizeof(queries[0]);
44-
45-
/* Valid JSON vectors (TEXT) — parsed via fvec_from_value text path,
46-
* which sets cleanup = sqlite3_free */
47-
static const char *json_vecs[] = {
48-
"[1.0, 0.0, 0.0, 0.0]", /* 4d */
49-
"[1.0, 2.0]", /* 2d */
50-
"[1.0]", /* 1d */
51-
};
52-
static const int nJsonVecs = sizeof(json_vecs) / sizeof(json_vecs[0]);
53-
54-
/* Valid blob vectors (BLOB) — parsed via fvec_from_value blob path,
55-
* which sets cleanup = fvec_cleanup_noop */
56-
static const float blob_vec[] = {1.0f, 0.0f, 0.0f, 0.0f};
57117

58-
uint8_t selector = data[0];
59-
int qIdx = selector % nQueries;
60-
/* Bits 3-4: select which valid vector and format for arg1 */
61-
int arg1_mode = (selector / nQueries) % 4;
62-
63-
const uint8_t *payload = data + 1;
64-
int payload_size = (int)(size - 1);
65-
66-
/* --- Test 1: valid arg1, fuzz arg2 --- */
67-
rc = sqlite3_prepare_v2(db, queries[qIdx], -1, &stmt, NULL);
68-
if (rc != SQLITE_OK) { sqlite3_close(db); return 0; }
69-
70-
/* Bind arg1 as either JSON text or blob */
71-
switch (arg1_mode) {
72-
case 0: /* JSON text — triggers sqlite3_free cleanup */
73-
sqlite3_bind_text(stmt, 1, json_vecs[0], -1, SQLITE_STATIC);
74-
break;
75-
case 1:
76-
sqlite3_bind_text(stmt, 1, json_vecs[1], -1, SQLITE_STATIC);
77-
break;
78-
case 2:
79-
sqlite3_bind_text(stmt, 1, json_vecs[2], -1, SQLITE_STATIC);
80-
break;
81-
case 3: /* blob — triggers noop cleanup */
82-
sqlite3_bind_blob(stmt, 1, blob_vec, sizeof(blob_vec), SQLITE_STATIC);
83-
break;
84-
}
85-
86-
/* Bind arg2 as fuzz blob (most likely to fail parsing for non-4-aligned sizes) */
87-
sqlite3_bind_blob(stmt, 2, payload, payload_size, SQLITE_STATIC);
88-
sqlite3_step(stmt);
89-
sqlite3_finalize(stmt);
90-
stmt = NULL;
91-
92-
/* --- Test 2: same but arg2 as fuzz text --- */
93-
rc = sqlite3_prepare_v2(db, queries[qIdx], -1, &stmt, NULL);
94-
if (rc != SQLITE_OK) { sqlite3_close(db); return 0; }
95-
96-
switch (arg1_mode) {
97-
case 0:
98-
sqlite3_bind_text(stmt, 1, json_vecs[0], -1, SQLITE_STATIC);
99-
break;
100-
case 1:
101-
sqlite3_bind_text(stmt, 1, json_vecs[1], -1, SQLITE_STATIC);
102-
break;
103-
case 2:
104-
sqlite3_bind_text(stmt, 1, json_vecs[2], -1, SQLITE_STATIC);
105-
break;
106-
case 3:
107-
sqlite3_bind_blob(stmt, 1, blob_vec, sizeof(blob_vec), SQLITE_STATIC);
108-
break;
109-
}
110-
111-
sqlite3_bind_text(stmt, 2, (const char *)payload, payload_size, SQLITE_STATIC);
112-
sqlite3_step(stmt);
113-
sqlite3_finalize(stmt);
114-
stmt = NULL;
115-
116-
/* --- Test 3: fuzz arg1, valid arg2 --- */
117-
rc = sqlite3_prepare_v2(db, queries[qIdx], -1, &stmt, NULL);
118-
if (rc != SQLITE_OK) { sqlite3_close(db); return 0; }
118+
/* Single-argument vector functions that call cleanup */
119+
static const char *one_arg[] = {
120+
"SELECT vec_f32(?)",
121+
"SELECT vec_int8(?)",
122+
"SELECT vec_bit(?)",
123+
"SELECT vec_length(?)",
124+
"SELECT vec_type(?)",
125+
"SELECT vec_to_json(?)",
126+
"SELECT vec_normalize(?)",
127+
"SELECT vec_quantize_binary(?)",
128+
};
119129

120-
sqlite3_bind_blob(stmt, 1, payload, payload_size, SQLITE_STATIC);
121-
sqlite3_bind_text(stmt, 2, json_vecs[0], -1, SQLITE_STATIC);
122-
sqlite3_step(stmt);
123-
sqlite3_finalize(stmt);
130+
int qIdx2 = b0 % 6;
131+
int qIdx1 = b0 % 8;
132+
int arg1_mode = b1 % 7;
133+
int arg2_mode = b2 % 7;
134+
135+
/*
136+
* Phase 1: Two-arg functions — fuzz arg2, valid arg1
137+
* Exercises: parse-failure cleanup of arg1 (the fixed bug),
138+
* type mismatch cleanup, dimension mismatch cleanup.
139+
*/
140+
/* arg2 as fuzz blob */
141+
run_query(db, two_arg[qIdx2], arg1_mode, 0,
142+
payload, payload_size, /*fuzz_arg=*/2, /*as_text=*/0);
143+
/* arg2 as fuzz text */
144+
run_query(db, two_arg[qIdx2], arg1_mode, 0,
145+
payload, payload_size, /*fuzz_arg=*/2, /*as_text=*/1);
146+
147+
/*
148+
* Phase 2: Two-arg functions — fuzz arg1, valid arg2
149+
* Exercises: parse-failure of arg1 (no cleanup needed), and
150+
* type/dimension mismatch when arg1 parses to unexpected type.
151+
*/
152+
run_query(db, two_arg[qIdx2], 0, arg2_mode,
153+
payload, payload_size, /*fuzz_arg=*/1, /*as_text=*/0);
154+
run_query(db, two_arg[qIdx2], 0, arg2_mode,
155+
payload, payload_size, /*fuzz_arg=*/1, /*as_text=*/1);
156+
157+
/*
158+
* Phase 3: Two-arg — both valid but deliberately mismatched types/dims.
159+
* arg1_mode and arg2_mode often produce different types or dimensions.
160+
* Exercises: type mismatch (lines 1035-1042) and dimension mismatch
161+
* (lines 1044-1051) with sqlite3_free cleanup on both sides.
162+
*/
163+
run_query(db, two_arg[qIdx2], arg1_mode, arg2_mode,
164+
NULL, 0, /*fuzz_arg=*/0, /*as_text=*/0);
165+
166+
/*
167+
* Phase 4: Single-arg functions — fuzz as blob and text.
168+
* Exercises: parse failure paths in vec_f32, vec_int8, vec_bit, etc.
169+
* Also exercises normal cleanup when fuzz data happens to be valid.
170+
*/
171+
run_query(db, one_arg[qIdx1], 0, 0,
172+
payload, payload_size, /*fuzz_arg=*/1, /*as_text=*/0);
173+
run_query(db, one_arg[qIdx1], 0, 0,
174+
payload, payload_size, /*fuzz_arg=*/1, /*as_text=*/1);
124175

125176
sqlite3_close(db);
126177
return 0;

0 commit comments

Comments
 (0)