git: f879eb3febb6 - main - libc/dbm: Differentiate between uninitialized and end-of-db cursors

From: Bojan Novković <bnovkov_at_FreeBSD.org>
Date: Sat, 20 Jun 2026 14:29:02 UTC
The branch main has been updated by bnovkov:

URL: https://cgit.FreeBSD.org/src/commit/?id=f879eb3febb6c294d684e8e7fecd5cc396ec5a28

commit f879eb3febb6c294d684e8e7fecd5cc396ec5a28
Author:     Bojan Novković <bnovkov@FreeBSD.org>
AuthorDate: 2026-06-19 15:24:05 +0000
Commit:     Bojan Novković <bnovkov@FreeBSD.org>
CommitDate: 2026-06-20 14:27:44 +0000

    libc/dbm: Differentiate between uninitialized and end-of-db cursors
    
    Commit `3a686b851f8f` fixed a `dbm_nextkey` edge case when using
    the function after reaching the end of the database, but it inadvertently
    broke the following `R_NEXT` behaviour:
    "If the cursor is not yet set, this is the same as the R_FIRST flag."
    
    Fix this by adding a new cursor constant that allows us to differentiate
    between an unset cursor and a cursor that overflowed.
    
    Reported by:    ae
    Fixes:  3a686b851f8f
    Sponsored by:   Klara, Inc.
    Differential Revision:  https://reviews.freebsd.org/D57670
    Reviewed by:    markj
---
 lib/libc/db/hash/hash.c              | 12 ++++++++----
 lib/libc/tests/db/dbm_nextkey_test.c | 33 +++++++++++++++++++++++++++++++++
 2 files changed, 41 insertions(+), 4 deletions(-)

diff --git a/lib/libc/db/hash/hash.c b/lib/libc/db/hash/hash.c
index 88a3ffeab828..b025a2310e8f 100644
--- a/lib/libc/db/hash/hash.c
+++ b/lib/libc/db/hash/hash.c
@@ -81,6 +81,10 @@ static void  swap_header_copy(HASHHDR *, HASHHDR *);
 #define	ERROR	(-1)
 #define	ABNORMAL (1)
 
+/* Cursor status */
+#define CURSOR_NOT_SET -1
+#define CURSOR_OVERFLOW -2
+
 #ifdef HASH_STATISTICS
 int hash_accesses, hash_collisions, hash_expansions, hash_overflows;
 #endif
@@ -180,7 +184,7 @@ __hash_open(const char *file, int flags, int mode,
 
 	hashp->new_file = new_table;
 	hashp->save_file = file && (flags & O_RDWR);
-	hashp->cbucket = -1;
+	hashp->cbucket = CURSOR_NOT_SET;
 	if (!(dbp = (DB *)malloc(sizeof(DB)))) {
 		save_errno = errno;
 		hdestroy(hashp);
@@ -711,11 +715,11 @@ hash_seq(const DB *dbp, DBT *key, DBT *data, u_int32_t flag)
 #ifdef HASH_STATISTICS
 	hash_accesses++;
 #endif
-	if (flag == R_FIRST) {
+	if (flag == R_FIRST || hashp->cbucket == CURSOR_NOT_SET) {
 		hashp->cbucket = 0;
 		hashp->cndx = 1;
 		hashp->cpage = NULL;
-	} else if (hashp->cbucket < 0) { /* R_NEXT */
+	} else if (hashp->cbucket <= CURSOR_OVERFLOW) {
 		return (ABNORMAL);
 	}
 next_bucket:
@@ -734,7 +738,7 @@ next_bucket:
 			}
 			hashp->cbucket = bucket;
 			if ((u_int32_t)hashp->cbucket > hashp->MAX_BUCKET) {
-				hashp->cbucket = -1;
+				hashp->cbucket = CURSOR_OVERFLOW;
 				return (ABNORMAL);
 			}
 		} else {
diff --git a/lib/libc/tests/db/dbm_nextkey_test.c b/lib/libc/tests/db/dbm_nextkey_test.c
index 67b745efb196..993cec8d7f85 100644
--- a/lib/libc/tests/db/dbm_nextkey_test.c
+++ b/lib/libc/tests/db/dbm_nextkey_test.c
@@ -45,9 +45,42 @@ ATF_TC_BODY(dbm_nextkey_test, tc)
 	dbm_close(db);
 }
 
+ATF_TC(dbm_nextkey_cursor_test);
+ATF_TC_HEAD(dbm_nextkey_cursor_test, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "Check that dbm_nextkey acts as if R_FIRST was passed if the cursor is not set");
+}
+
+ATF_TC_BODY(dbm_nextkey_cursor_test, tc)
+{
+	DBM *db;
+	datum key, data;
+
+	data.dptr = "bar";
+	data.dsize = strlen("bar");
+	key.dptr = "foo";
+	key.dsize = strlen("foo");
+
+	db = dbm_open(path, O_RDWR | O_CREAT, 0755);
+	ATF_CHECK(db != NULL);
+	ATF_REQUIRE(atf_utils_file_exists(dbname));
+	ATF_REQUIRE(dbm_store(db, key, data, DBM_INSERT) != -1);
+	dbm_close(db);
+
+	db = dbm_open(path, O_RDWR | O_CREAT, 0755);
+	ATF_CHECK(db != NULL);
+	ATF_REQUIRE(atf_utils_file_exists(dbname));
+	key = dbm_nextkey(db);
+	ATF_REQUIRE(key.dptr != NULL);
+
+	dbm_close(db);
+}
+
 ATF_TP_ADD_TCS(tp)
 {
 	ATF_TP_ADD_TC(tp, dbm_nextkey_test);
+	ATF_TP_ADD_TC(tp, dbm_nextkey_cursor_test);
 
 	return (atf_no_error());
 }