1 /*-
2 * Copyright (c) 2026. Klara, Inc.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7 #include <sys/types.h>
8 #include <sys/wait.h>
9
10 #include <netinet/in.h>
11
12 #include <atf-c.h>
13 #include <db.h>
14 #include <fcntl.h>
15 #include <signal.h>
16 #include <stdint.h>
17 #include <stdio.h>
18 #include <stdlib.h>
19
20 /*
21 * The internal db/hash/hash.h header is needed to
22 * avoid hardcoding header structure offsets.
23 */
24 #include "hash.h"
25
26 #define SET_HDR_VAR(hdr, field, val) (hdr)->field = htonl((uint32_t)val)
27 #define GET_HDR_VAR(hdr, field) ((uint32_t)ntohl((hdr)->field);)
28
29 static const char *dbname = "tmp.db";
30
31 /* Create a database file with one entry. */
32 static void
create_db(void)33 create_db(void)
34 {
35 DB *db;
36 DBT key, val;
37
38 key.data = "foo";
39 key.size = strlen("foo");
40
41 val.data = "bar";
42 val.size = strlen("bar");
43
44 if (atf_utils_file_exists(dbname))
45 unlink(dbname);
46 db = dbopen(dbname, O_CREAT | O_RDWR | O_TRUNC, 0755, DB_HASH, NULL);
47 ATF_CHECK(db != NULL);
48 ATF_REQUIRE(atf_utils_file_exists(dbname));
49
50 ATF_REQUIRE(db->put(db, &key, &val, 0) == 0);
51
52 db->close(db);
53 }
54
55 static void
read_hdr(HASHHDR * hdr)56 read_hdr(HASHHDR *hdr)
57 {
58 int fd;
59
60 ATF_REQUIRE(atf_utils_file_exists(dbname));
61 fd = open(dbname, O_RDONLY);
62 ATF_CHECK(fd != -1);
63 ATF_CHECK(read(fd, hdr, sizeof(*hdr)) == sizeof(*hdr));
64 close(fd);
65 }
66
67 static void
write_hdr(HASHHDR * hdr)68 write_hdr(HASHHDR *hdr)
69 {
70 int fd;
71
72 ATF_REQUIRE(atf_utils_file_exists(dbname));
73 fd = open(dbname, O_WRONLY);
74 ATF_CHECK(fd != -1);
75 ATF_CHECK(write(fd, hdr, sizeof(*hdr)) == sizeof(*hdr));
76 close(fd);
77 }
78
79 ATF_TC(db_hash_ovflw_point_test);
ATF_TC_HEAD(db_hash_ovflw_point_test,tc)80 ATF_TC_HEAD(db_hash_ovflw_point_test, tc)
81 {
82 atf_tc_set_md_var(tc, "descr",
83 "Test hash(3) operations with a corrupted 'ovfl_point' header variable.");
84 }
85
ATF_TC_BODY(db_hash_ovflw_point_test,tc)86 ATF_TC_BODY(db_hash_ovflw_point_test, tc)
87 {
88 HASHHDR hdr;
89
90 create_db();
91
92 read_hdr(&hdr);
93 /*
94 * An unvalidated 'ovfl_point' variable may trigger
95 * an OOB read from the SPARES field.
96 */
97 SET_HDR_VAR(&hdr, ovfl_point, NCACHED + 1);
98 write_hdr(&hdr);
99
100 ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
101 }
102
103 ATF_TC(db_hash_bpages_test);
ATF_TC_HEAD(db_hash_bpages_test,tc)104 ATF_TC_HEAD(db_hash_bpages_test, tc)
105 {
106 atf_tc_set_md_var(tc, "descr",
107 "Test hash(3) operations with a corrupted 'spares' header variable.");
108 }
109
ATF_TC_BODY(db_hash_bpages_test,tc)110 ATF_TC_BODY(db_hash_bpages_test, tc)
111 {
112 HASHHDR hdr;
113
114 create_db();
115
116 read_hdr(&hdr);
117 /*
118 * An unvalidated combination of the 'ovfl_point' variable
119 * and the 'spares' array may be used to manipulate
120 * a memset in _hash_open.
121 */
122 SET_HDR_VAR(&hdr, ovfl_point, 0);
123 hdr.spares[0] = htonl(0x10000000UL);
124 write_hdr(&hdr);
125
126 ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
127 }
128
129 ATF_TC(db_hash_bsize_test);
ATF_TC_HEAD(db_hash_bsize_test,tc)130 ATF_TC_HEAD(db_hash_bsize_test, tc)
131 {
132 atf_tc_set_md_var(tc, "descr",
133 "Test hash(3) operations with a corrupted 'bsize' header variable.");
134 }
135
ATF_TC_BODY(db_hash_bsize_test,tc)136 ATF_TC_BODY(db_hash_bsize_test, tc)
137 {
138 HASHHDR hdr;
139
140 create_db();
141
142 read_hdr(&hdr);
143 /*
144 * An unvalidated 'bsize' variable may be
145 * used to manipulate a memset in _hash_open.
146 */
147 SET_HDR_VAR(&hdr, bsize, 0x100000);
148 write_hdr(&hdr);
149
150 ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
151 }
152
153 ATF_TC(db_hash_masks_test);
ATF_TC_HEAD(db_hash_masks_test,tc)154 ATF_TC_HEAD(db_hash_masks_test, tc)
155 {
156 atf_tc_set_md_var(tc, "descr",
157 "Test hash(3) operations with corrupted '{high,low}_mask' header variables.");
158 }
159
ATF_TC_BODY(db_hash_masks_test,tc)160 ATF_TC_BODY(db_hash_masks_test, tc)
161 {
162 HASHHDR hdr;
163
164 /* 'high_mask' must be greater than 'low_mask'. */
165 create_db();
166 read_hdr(&hdr);
167 SET_HDR_VAR(&hdr, high_mask, 0x1);
168 SET_HDR_VAR(&hdr, low_mask, 0xF);
169 write_hdr(&hdr);
170 ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
171
172 /* 'high_mask' and 'low_mask' must be derived from power-of-2 values. */
173 create_db();
174 read_hdr(&hdr);
175 SET_HDR_VAR(&hdr, high_mask, 0x13);
176 write_hdr(&hdr);
177 ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
178
179 create_db();
180 read_hdr(&hdr);
181 SET_HDR_VAR(&hdr, high_mask, 0xFF);
182 SET_HDR_VAR(&hdr, low_mask, 0x13);
183 write_hdr(&hdr);
184 ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
185 }
186
187 ATF_TC(db_hash_call_hash_oob_test);
ATF_TC_HEAD(db_hash_call_hash_oob_test,tc)188 ATF_TC_HEAD(db_hash_call_hash_oob_test, tc)
189 {
190 atf_tc_set_md_var(tc, "descr",
191 "Attempt to trigger an OOB read with corrupted '{high,low}_mask' header variables.");
192 }
193
ATF_TC_BODY(db_hash_call_hash_oob_test,tc)194 ATF_TC_BODY(db_hash_call_hash_oob_test, tc)
195 {
196 DBT key, val;
197 HASHHDR hdr;
198 DB *db;
199
200 key.data = "foo";
201 key.size = strlen("foo");
202
203 /*
204 * Invalid values of the '{high,low}_mask' header variables
205 * will cause __call_hash to return OOB bucket indices.
206 */
207 create_db();
208 read_hdr(&hdr);
209 SET_HDR_VAR(&hdr, low_mask, 0xFFFF);
210 SET_HDR_VAR(&hdr, high_mask, 0xFFFFF);
211 write_hdr(&hdr);
212 db = dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL);
213 ATF_REQUIRE(db != NULL);
214 /* Attempt to trigger an OOB read. */
215 ATF_REQUIRE(db->get(db, &key, &val, 0) != 0);
216 }
217
ATF_TP_ADD_TCS(tp)218 ATF_TP_ADD_TCS(tp)
219 {
220 ATF_TP_ADD_TC(tp, db_hash_ovflw_point_test);
221 ATF_TP_ADD_TC(tp, db_hash_bpages_test);
222 ATF_TP_ADD_TC(tp, db_hash_bsize_test);
223 ATF_TP_ADD_TC(tp, db_hash_masks_test);
224 ATF_TP_ADD_TC(tp, db_hash_call_hash_oob_test);
225
226 return (atf_no_error());
227 }
228