/src/mdbtools/src/fuzz/fuzz_write.c
Line | Count | Source |
1 | | /* |
2 | | * fuzz_write.c - mdbtools row-write / round-trip harness. |
3 | | * |
4 | | * Threat model |
5 | | * ============ |
6 | | * Untrusted input: attacker-supplied Jet database bytes - the whole fuzz input |
7 | | * IS the database image (established mdbtestdata corpus doubles as seed |
8 | | * corpus). Tools in the mdbtools suite (mdb-import, mdb-create, ODBC writers) |
9 | | * open attacker-supplied database files in read-write mode and encode values |
10 | | * into pages, so the page structures cross the trust boundary as data. |
11 | | * |
12 | | * Entry points exercised: |
13 | | * - mdb_read_table/mdb_read_columns/mdb_read_indices: definition parsing |
14 | | * - mdb_insert_row(): the mdb-import-shaped write path (pack, add to page, |
15 | | * update indexes, write page back). The database is opened MDB_WRITABLE |
16 | | * over a private writable copy of the input, so writes stay deterministic |
17 | | * and never touch libFuzzer's read-only input buffer. |
18 | | * - mdb_create_temp_table/mdb_temp_table_add_col/mdb_pack_row/ |
19 | | * mdb_add_row_to_pg: the worktable component used for computed result |
20 | | * tables (internally consistent pages, so the page-write paths run without |
21 | | * the corrupt-page defect documented as mdbtools-5). |
22 | | * - mdb_rewind_table/mdb_fetch_row: the decode side of the round trip. |
23 | | * |
24 | | * Expected bug classes: OOB writes in row packing/page layout, int overflows |
25 | | * in offset/size arithmetic, memory safety in index updates, corruption-induced |
26 | | * crashes on the decode side of the round trip. |
27 | | * |
28 | | * Known upstream defects routed around by battery design (see the pair report): |
29 | | * mdbtools-4 (mdb_index_swap_n SEGV, index.c:436, when inserting into tables |
30 | | * that own a primary index - row writes are restricted to non-indexed tables) |
31 | | * and mdbtools-5 (negative-size memcpy at write.c:675 in mdb_add_row_to_pg's |
32 | | * copy-existing-rows loop - the direct pack/add variant over real pages is not |
33 | | * exercised; temp tables keep it on internally consistent pages). |
34 | | */ |
35 | | #include "mdbtools.h" |
36 | | #include <stdint.h> |
37 | | #include <stdlib.h> |
38 | | #include <string.h> |
39 | | |
40 | 7.09k | #define MAX_TABLES 4 |
41 | 1.23k | #define MAX_INSERT_ROWS 2 |
42 | 44.3k | #define MAX_READBACK 64 |
43 | 824 | #define MAX_FIELDS 4 |
44 | | |
45 | | static unsigned int fill_field(MdbField *field, MdbColumn *col, |
46 | | uint8_t *vbuf, unsigned int seed) |
47 | 3.29k | { |
48 | 3.29k | unsigned int i, need; |
49 | 3.29k | int fixed = mdb_is_fixed_col(col) ? 1 : 0; |
50 | 3.29k | int siz; |
51 | | |
52 | 3.29k | if (fixed) { |
53 | 2.65k | siz = mdb_col_fixed_size(col); |
54 | 2.65k | if (siz < 0 || siz > 16) |
55 | 130 | siz = 4; |
56 | 2.65k | need = (unsigned int)siz; |
57 | 2.65k | } else { |
58 | 648 | siz = 8; |
59 | 648 | need = (unsigned int)siz; |
60 | 648 | } |
61 | 17.7k | for (i = 0; i < need; i++) |
62 | 14.4k | vbuf[i] = (uint8_t)(seed * 31 + i * 7); |
63 | 3.29k | mdb_fill_temp_field(field, vbuf, siz, fixed, 0, 0, (int)seed); |
64 | 3.29k | return need; |
65 | 3.29k | } |
66 | | |
67 | | static void row_write_battery(MdbTableDef *table) |
68 | 585 | { |
69 | 585 | MdbField fields[MAX_FIELDS]; |
70 | 585 | uint8_t vbuf[MAX_FIELDS][16]; |
71 | 585 | unsigned int n, f; |
72 | 585 | int has_primary_idx = 0; |
73 | | |
74 | 10.8k | for (n = 0; n < table->num_idxs; n++) { |
75 | 10.3k | MdbIndex *idx = (MdbIndex *)g_ptr_array_index(table->indices, n); |
76 | 10.3k | if (idx->index_type == 1) |
77 | 569 | has_primary_idx = 1; |
78 | 10.3k | } |
79 | | |
80 | | /* findings mdbtools-4 and mdbtools-5: the write machinery crashes on |
81 | | * attacker-controlled index pages (mdb_index_swap_n SEGV) and on crafted |
82 | | * data pages (negative-size memcpy in mdb_add_row_to_pg's copy loop). |
83 | | * Route around: only non-indexed tables get row writes; indexed tables |
84 | | * get readbacks only. Re-enable once upstream hardens both paths. */ |
85 | 585 | if (!has_primary_idx && table->num_cols > 0) { |
86 | 413 | unsigned int ncols = table->num_cols < MAX_FIELDS ? |
87 | 411 | table->num_cols : MAX_FIELDS; |
88 | 1.23k | for (n = 0; n < MAX_INSERT_ROWS; n++) { |
89 | 826 | unsigned int used = 0; |
90 | 4.12k | for (f = 0; f < ncols; f++) { |
91 | 3.29k | MdbColumn *col = (MdbColumn *)g_ptr_array_index( |
92 | 3.29k | table->columns, f); |
93 | 3.29k | used += fill_field(&fields[f], col, vbuf[f], |
94 | 3.29k | n * 16 + f); |
95 | 3.29k | } |
96 | 826 | mdb_insert_row(table, (int)ncols, fields); |
97 | 826 | } |
98 | 413 | } |
99 | | |
100 | | /* decode side of the round trip: read back whatever is on the pages */ |
101 | 585 | { |
102 | 585 | char bound[MDB_BIND_SIZE]; |
103 | 585 | unsigned long rows = 0; |
104 | 585 | mdb_rewind_table(table); |
105 | 585 | if (mdb_bind_column(table, 1, bound, NULL) != -1) { |
106 | 15.7k | while (rows++ < MAX_READBACK && mdb_fetch_row(table)) |
107 | 15.2k | bound[0] ^= (unsigned char)rows; |
108 | 556 | } |
109 | 585 | (void)bound[0]; |
110 | 585 | } |
111 | 585 | } |
112 | | |
113 | | static void temp_table_battery(MdbHandle *mdb) |
114 | 2.12k | { |
115 | 2.12k | static const int types[2] = { MDB_LONGINT, MDB_TEXT }; |
116 | 2.12k | unsigned char row_buffer[MDB_PGSIZE]; |
117 | 2.12k | char bound[MDB_BIND_SIZE]; |
118 | 2.12k | MdbColumn col; |
119 | 2.12k | MdbField field; |
120 | 2.12k | uint8_t vbuf[16]; |
121 | 2.12k | MdbTableDef *ttable; |
122 | 2.12k | unsigned int n; |
123 | 2.12k | int row_size; |
124 | | |
125 | 2.12k | ttable = mdb_create_temp_table(mdb, "#fuzz"); |
126 | 6.37k | for (n = 0; n < 2; n++) { |
127 | 4.24k | char name[16]; |
128 | 4.24k | snprintf(name, sizeof(name), "c%u", n); |
129 | 4.24k | mdb_fill_temp_col(&col, name, 16, types[n], |
130 | 4.24k | (types[n] != MDB_TEXT) ? 1 : 0); |
131 | 4.24k | mdb_temp_table_add_col(ttable, &col); |
132 | 4.24k | } |
133 | 2.12k | mdb_temp_columns_end(ttable); |
134 | 6.37k | for (n = 0; n < 2; n++) { |
135 | 4.24k | memset(vbuf, (int)(n + 1), sizeof(vbuf)); |
136 | 4.24k | mdb_fill_temp_field(&field, vbuf, (int)sizeof(vbuf), 1, 0, 0, 0); |
137 | 4.24k | row_size = mdb_pack_row(ttable, row_buffer, 1, &field); |
138 | 4.24k | if (row_size > 0) |
139 | 4.24k | mdb_add_row_to_pg(ttable, row_buffer, row_size); |
140 | 4.24k | } |
141 | 2.12k | { |
142 | 2.12k | unsigned long rows = 0; |
143 | 2.12k | mdb_rewind_table(ttable); |
144 | 2.12k | if (mdb_bind_column(ttable, 1, bound, NULL) != -1) { |
145 | 6.37k | while (rows++ < MAX_READBACK && mdb_fetch_row(ttable)) |
146 | 4.24k | bound[0] ^= (unsigned char)rows; |
147 | 2.12k | } |
148 | 2.12k | (void)bound[0]; |
149 | 2.12k | } |
150 | 2.12k | mdb_free_tabledef(ttable); |
151 | 2.12k | } |
152 | | |
153 | | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) |
154 | 2.19k | { |
155 | 2.19k | unsigned char *dbcopy; |
156 | 2.19k | MdbHandle *mdb; |
157 | 2.19k | MdbCatalogEntry *entry; |
158 | 2.19k | unsigned int t; |
159 | | |
160 | 2.19k | if (size < 8) |
161 | 4 | return 0; |
162 | | |
163 | | /* writes must never touch libFuzzer's read-only input buffer */ |
164 | 2.19k | dbcopy = (unsigned char *)malloc(size); |
165 | 2.19k | if (!dbcopy) |
166 | 0 | return 0; |
167 | 2.19k | memcpy(dbcopy, data, size); |
168 | | |
169 | 2.19k | mdb = mdb_open_buffer(dbcopy, (int)size, MDB_WRITABLE); |
170 | 2.19k | if (!mdb) { |
171 | 66 | free(dbcopy); |
172 | 66 | return 0; |
173 | 66 | } |
174 | 2.12k | mdb_read_catalog(mdb, MDB_TABLE); |
175 | | |
176 | 3.54k | for (t = 0; t < MAX_TABLES && t < (unsigned int)mdb->num_catalog; t++) { |
177 | 1.42k | MdbTableDef *table; |
178 | | |
179 | 1.42k | entry = (MdbCatalogEntry *)g_ptr_array_index(mdb->catalog, t); |
180 | 1.42k | table = mdb_read_table(entry); |
181 | 1.42k | if (!table) |
182 | 827 | continue; |
183 | 597 | if (!mdb_read_columns(table)) { |
184 | 12 | mdb_free_tabledef(table); |
185 | 12 | continue; |
186 | 12 | } |
187 | 585 | mdb_read_indices(table); |
188 | 585 | row_write_battery(table); |
189 | 585 | mdb_free_tabledef(table); |
190 | 585 | } |
191 | 2.12k | temp_table_battery(mdb); |
192 | | |
193 | 2.12k | mdb_close(mdb); |
194 | 2.12k | free(dbcopy); |
195 | 2.12k | return 0; |
196 | 2.19k | } |