Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F167424741
D58822.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
D58822.diff
View Options
diff --git a/lib/libc/db/hash/hash.c b/lib/libc/db/hash/hash.c
--- a/lib/libc/db/hash/hash.c
+++ b/lib/libc/db/hash/hash.c
@@ -155,12 +155,32 @@
RETURN_ERROR(EFTYPE, error1);
if ((int32_t)hashp->hash(CHARKEY, sizeof(CHARKEY)) != hashp->H_CHARKEY)
RETURN_ERROR(EFTYPE, error1);
+ /* Validate the remaining header variables. */
+ if (hashp->OVFL_POINT < 0 || hashp->OVFL_POINT >= NCACHED)
+ RETURN_ERROR(EFTYPE, error1);
+ if (hashp->LAST_FREED < 0 && hashp->LAST_FREED >= NCACHED)
+ RETURN_ERROR(EFTYPE, error1);
+ if (hashp->BSIZE < 0 || hashp->BSIZE > MAX_BSIZE)
+ RETURN_ERROR(EFTYPE, error1);
+ /* Both masks should be derived from power-of-2 values. */
+ if (((hashp->HIGH_MASK + 1) & hashp->HIGH_MASK) != 0 ||
+ ((hashp->LOW_MASK + 1) & hashp->LOW_MASK) != 0)
+ RETURN_ERROR(EFTYPE, error1);
+ if (hashp->LOW_MASK >= hashp->HIGH_MASK)
+ RETURN_ERROR(EFTYPE, error1);
+
+ hashp->BSHIFT = __log2(hashp->BSIZE);
+ hashp->SGSIZE = DEF_SEGSIZE;
+ hashp->SSHIFT = DEF_SEGSIZE_SHIFT;
/*
* Figure out how many segments we need. Max_Bucket is the
* maximum bucket number, so the number of buckets is
* max_bucket + 1.
*/
nsegs = howmany(hashp->MAX_BUCKET + 1, hashp->SGSIZE);
+ /* Verify that DSIZE can hold the required number of segments. */
+ if (hashp->DSIZE < nsegs)
+ RETURN_ERROR(EFTYPE, error1);
if (alloc_segs(hashp, nsegs))
/*
* If alloc_segs fails, table will have been destroyed
@@ -171,6 +191,8 @@
bpages = (hashp->SPARES[hashp->OVFL_POINT] +
(hashp->BSIZE << BYTE_SHIFT) - 1) >>
(hashp->BSHIFT + BYTE_SHIFT);
+ if (bpages < 0 || bpages >= NCACHED)
+ RETURN_ERROR(EFTYPE, error1);
hashp->nmaps = bpages;
(void)memset(&hashp->mapp[0], 0, bpages * sizeof(u_int32_t *));
@@ -886,26 +908,27 @@
int save_errno;
- if ((hashp->dir =
- calloc(hashp->DSIZE, sizeof(SEGMENT *))) == NULL) {
- save_errno = errno;
- (void)hdestroy(hashp);
- errno = save_errno;
- return (-1);
+ if (nsegs < 0) {
+ errno = EINVAL;
+ goto err_out;
}
+ if ((hashp->dir = calloc(hashp->DSIZE, sizeof(SEGMENT *))) == NULL)
+ goto err_out;
hashp->nsegs = nsegs;
if (nsegs == 0)
return (0);
/* Allocate segments */
- if ((store = calloc(nsegs << hashp->SSHIFT, sizeof(SEGMENT))) == NULL) {
- save_errno = errno;
- (void)hdestroy(hashp);
- errno = save_errno;
- return (-1);
- }
+ if ((store = calloc(nsegs << hashp->SSHIFT, sizeof(SEGMENT))) == NULL)
+ goto err_out;
for (i = 0; i < nsegs; i++)
hashp->dir[i] = &store[i << hashp->SSHIFT];
return (0);
+
+err_out:
+ save_errno = errno;
+ (void)hdestroy(hashp);
+ errno = save_errno;
+ return (-1);
}
#if BYTE_ORDER == LITTLE_ENDIAN
diff --git a/lib/libc/db/hash/hash_buf.c b/lib/libc/db/hash/hash_buf.c
--- a/lib/libc/db/hash/hash_buf.c
+++ b/lib/libc/db/hash/hash_buf.c
@@ -100,10 +100,10 @@
BUFHEAD *prev_bp, /* If prev_bp set, indicates a new overflow page. */
int newpage)
{
- BUFHEAD *bp;
+ int is_disk, segment_ndx, dir_ndx;
u_int32_t is_disk_mask;
- int is_disk, segment_ndx;
SEGMENT segp;
+ BUFHEAD *bp;
is_disk = 0;
is_disk_mask = 0;
@@ -116,9 +116,21 @@
} else {
/* Grab buffer out of directory */
segment_ndx = addr & (hashp->SGSIZE - 1);
-
+ dir_ndx = addr >> hashp->SSHIFT;
+ if (dir_ndx >= hashp->nsegs) {
+ /*
+ * A bucket address could theoretically have been
+ * generated using maliciously crafted header values
+ * aimed at __call_hash.
+ */
+ fprintf(stderr,
+ "%s: hashp->dir array index %d out of bounds, possible"
+ " database corruption\n",
+ __func__, dir_ndx);
+ abort();
+ }
/* valid segment ensured by __call_hash() */
- segp = hashp->dir[addr >> hashp->SSHIFT];
+ segp = hashp->dir[dir_ndx];
#ifdef DEBUG
assert(segp != NULL);
#endif
diff --git a/lib/libc/db/hash/hash_page.c b/lib/libc/db/hash/hash_page.c
--- a/lib/libc/db/hash/hash_page.c
+++ b/lib/libc/db/hash/hash_page.c
@@ -666,6 +666,13 @@
/* Look through all the free maps to find the first free block */
first_page = hashp->LAST_FREED >>(hashp->BSHIFT + BYTE_SHIFT);
for ( i = first_page; i <= free_page; i++ ) {
+ if (i < 0 || i >= NCACHED) {
+ fprintf(stderr,
+ "%s: mapp array index %d out of bounds, possible"
+ "database corruption\n",
+ __func__, i);
+ abort();
+ }
if (!(freep = (u_int32_t *)hashp->mapp[i]) &&
!(freep = fetch_bitmap(hashp, i)))
return (0);
@@ -688,13 +695,18 @@
goto found;
}
+#define OVMSG "HASH: Out of overflow pages. Increase page size\n"
+ if (splitnum >= NCACHED) {
+ (void)_write(STDERR_FILENO, OVMSG, sizeof(OVMSG) - 1);
+ errno = EFBIG;
+ return (0);
+ }
/* No Free Page Found */
hashp->LAST_FREED = hashp->SPARES[splitnum];
hashp->SPARES[splitnum]++;
offset = hashp->SPARES[splitnum] -
(splitnum ? hashp->SPARES[splitnum - 1] : 0);
-#define OVMSG "HASH: Out of overflow pages. Increase page size\n"
if (offset > SPLITMASK) {
if (++splitnum >= NCACHED) {
(void)_write(STDERR_FILENO, OVMSG, sizeof(OVMSG) - 1);
diff --git a/lib/libc/tests/db/Makefile b/lib/libc/tests/db/Makefile
--- a/lib/libc/tests/db/Makefile
+++ b/lib/libc/tests/db/Makefile
@@ -10,12 +10,13 @@
ATF_TESTS_C+= dbm_open_test
ATF_TESTS_C+= dbm_perm_test
ATF_TESTS_C+= dbm_nextkey_test
+ATF_TESTS_C+= db_hash_tamper_test
NETBSD_ATF_TESTS_C+= db_hash_seq_test
NETBSD_ATF_TESTS_SH+= db_test
ATF_TESTS_SH_SED_db_test= -e 's,/bin/csh,/bin/cat,g'
-CFLAGS+= -I${SRCTOP}/lib/libc/db/btree
+CFLAGS+= -I${SRCTOP}/lib/libc/db/btree -I${SRCTOP}/lib/libc/db/hash
.include "../Makefile.netbsd-tests"
diff --git a/lib/libc/tests/db/db_hash_tamper_test.c b/lib/libc/tests/db/db_hash_tamper_test.c
new file mode 100644
--- /dev/null
+++ b/lib/libc/tests/db/db_hash_tamper_test.c
@@ -0,0 +1,238 @@
+/*-
+ * Copyright (c) 2026. Klara, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <netinet/in.h>
+
+#include <atf-c.h>
+#include <db.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+/*
+ * The internal db/hash/hash.h header is needed to
+ * avoid hardcoding header structure offsets.
+ */
+#include "hash.h"
+
+#define SET_HDR_VAR(hdr, field, val) (hdr)->field = htonl((uint32_t)val)
+#define GET_HDR_VAR(hdr, field) ((uint32_t)ntohl((hdr)->field);)
+
+static const char *dbname = "tmp.db";
+
+/* Create a database file with one entry. */
+static void
+create_db(void)
+{
+ DB *db;
+ DBT key, val;
+
+ key.data = "foo";
+ key.size = strlen("foo");
+
+ val.data = "bar";
+ val.size = strlen("bar");
+
+ if (atf_utils_file_exists(dbname))
+ unlink(dbname);
+ db = dbopen(dbname, O_CREAT | O_RDWR | O_TRUNC, 0755, DB_HASH, NULL);
+ ATF_CHECK(db != NULL);
+ ATF_REQUIRE(atf_utils_file_exists(dbname));
+
+ ATF_REQUIRE(db->put(db, &key, &key, 0) == 0);
+
+ db->close(db);
+}
+
+static void
+read_hdr(HASHHDR *hdr)
+{
+ int fd;
+
+ ATF_REQUIRE(atf_utils_file_exists(dbname));
+ fd = open(dbname, O_RDONLY);
+ ATF_CHECK(fd != -1);
+ ATF_CHECK(read(fd, hdr, sizeof(*hdr)) == sizeof(*hdr));
+ close(fd);
+}
+
+static void
+write_hdr(HASHHDR *hdr)
+{
+ int fd;
+
+ ATF_REQUIRE(atf_utils_file_exists(dbname));
+ fd = open(dbname, O_WRONLY);
+ ATF_CHECK(fd != -1);
+ ATF_CHECK(write(fd, hdr, sizeof(*hdr)) == sizeof(*hdr));
+ close(fd);
+}
+
+ATF_TC(db_hash_ovflw_point_test);
+ATF_TC_HEAD(db_hash_ovflw_point_test, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test hash(3) operations with a corrupted 'ovfl_point' header variable.");
+}
+
+ATF_TC_BODY(db_hash_ovflw_point_test, tc)
+{
+ HASHHDR hdr;
+
+ create_db();
+
+ read_hdr(&hdr);
+ /*
+ * An unvalidated 'ovfl_point' variable may trigger
+ * an OOB read from the SPARES field.
+ */
+ SET_HDR_VAR(&hdr, ovfl_point, NCACHED + 1);
+ write_hdr(&hdr);
+
+ ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
+}
+
+ATF_TC(db_hash_bpages_test);
+ATF_TC_HEAD(db_hash_bpages_test, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test hash(3) operations with a corrupted 'spares' header variable.");
+}
+
+ATF_TC_BODY(db_hash_bpages_test, tc)
+{
+ HASHHDR hdr;
+
+ create_db();
+
+ read_hdr(&hdr);
+ /*
+ * An unvalidated combination of the 'ovfl_point' variable
+ * and the 'spares' array may be used to manipulate
+ * a memset in _hash_open.
+ */
+ SET_HDR_VAR(&hdr, ovfl_point, 0);
+ hdr.spares[0] = htonl(0x10000000UL);
+ write_hdr(&hdr);
+
+ ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
+}
+
+ATF_TC(db_hash_bsize_test);
+ATF_TC_HEAD(db_hash_bsize_test, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test hash(3) operations with a corrupted 'bsize' header variable.");
+}
+
+ATF_TC_BODY(db_hash_bsize_test, tc)
+{
+ HASHHDR hdr;
+
+ create_db();
+
+ read_hdr(&hdr);
+ /*
+ * An unvalidated 'bsize' variable may be
+ * used to manipulate a memset in _hash_open.
+ */
+ SET_HDR_VAR(&hdr, bsize, 0x100000);
+ write_hdr(&hdr);
+
+ ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
+}
+
+ATF_TC(db_hash_masks_test);
+ATF_TC_HEAD(db_hash_masks_test, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test hash(3) operations with corrupted '{high,low}_mask' header variables.");
+}
+
+ATF_TC_BODY(db_hash_masks_test, tc)
+{
+ HASHHDR hdr;
+
+ /* 'high_mask' must be greater than 'low_mask'. */
+ create_db();
+ read_hdr(&hdr);
+ SET_HDR_VAR(&hdr, high_mask, 0x1);
+ SET_HDR_VAR(&hdr, low_mask, 0xF);
+ write_hdr(&hdr);
+ ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
+
+ /* 'high_mask' and 'low_mask' must be derived from power-of-2 values. */
+ create_db();
+ read_hdr(&hdr);
+ SET_HDR_VAR(&hdr, high_mask, 0x13);
+ write_hdr(&hdr);
+ ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
+
+ create_db();
+ read_hdr(&hdr);
+ SET_HDR_VAR(&hdr, high_mask, 0xFF);
+ SET_HDR_VAR(&hdr, low_mask, 0x13);
+ write_hdr(&hdr);
+ ATF_REQUIRE(dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL) == NULL);
+}
+
+ATF_TC(db_hash_call_hash_oob_test);
+ATF_TC_HEAD(db_hash_call_hash_oob_test, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Attempt to trigger an OOB read with corrupted '{high,low}_mask' header variables.");
+}
+
+ATF_TC_BODY(db_hash_call_hash_oob_test, tc)
+{
+ DBT key, val;
+ HASHHDR hdr;
+ int status;
+ pid_t pid;
+ DB *db;
+
+ pid = fork();
+ if (pid == 0) {
+ key.data = "foo";
+ key.size = strlen("foo");
+
+ /*
+ * Invalid values of the '{high,low}_mask' header variables
+ * will cause __call_hash to return OOB bucket indices.
+ */
+ create_db();
+ read_hdr(&hdr);
+ SET_HDR_VAR(&hdr, low_mask, 0xFFFF);
+ SET_HDR_VAR(&hdr, high_mask, 0xFFFFF);
+ write_hdr(&hdr);
+ db = dbopen(dbname, O_RDONLY, 0755, DB_HASH, NULL);
+ ATF_REQUIRE(db != NULL);
+ /* Attempt to trigger an OOB read. */
+ db->get(db, &key, &val, 0);
+ exit(0);
+ }
+ ATF_REQUIRE_MSG(pid > 0, "fork() failed");
+ ATF_CHECK_EQ(pid, waitpid(pid, &status, 0));
+ ATF_REQUIRE_MSG(WIFSIGNALED(status),
+ "dbopen should've detected the corrupted database header.");
+ ATF_CHECK_EQ_MSG(WTERMSIG(status), SIGABRT, "got %d", WTERMSIG(status));
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, db_hash_ovflw_point_test);
+ ATF_TP_ADD_TC(tp, db_hash_bpages_test);
+ ATF_TP_ADD_TC(tp, db_hash_bsize_test);
+ ATF_TP_ADD_TC(tp, db_hash_masks_test);
+ ATF_TP_ADD_TC(tp, db_hash_call_hash_oob_test);
+
+ return (atf_no_error());
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Aug 22, 4:04 PM (7 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
37096062
Default Alt Text
D58822.diff (11 KB)
Attached To
Mode
D58822: db/hash: Harden hash(3) database code
Attached
Detach File
Event Timeline
Log In to Comment