Page MenuHomeFreeBSD

D58822.diff
No OneTemporary

D58822.diff

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

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)

Event Timeline