Coverage Report

Created: 2026-11-12 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}