Compile the following source against 4.5.20 as the producer and against 5.3.34 as the reader and writer. Save it as repro-native.c.
#include <db.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/*
* Expected: DB->get finds a stored key after the comparator reports equality,
* and DB_NOOVERWRITE returns DB_KEYEXIST without adding a second record.
* Each control removes only DB->set_h_compare from the corresponding trigger.
*/
#define PAGE_SIZE 512 /* Page size selected by the producer. */
#define PAGE_TYPE_OFFSET 25 /* PAGE.type byte in both tested formats. */
#define P_HASHMETA 8 /* Exact libdb P_HASHMETA page-type value. */
#define P_HASH_UNSORTED 2 /* Exact libdb P_HASH_UNSORTED page-type value. */
#define P_HASH 13 /* Exact libdb P_HASH page-type value. */
#define ACCOUNT_COUNT 20
#if DB_VERSION_MAJOR > 4 || \
(DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 6)
static unsigned long comparator_calls;
static unsigned long comparator_equalities;
#endif
static void
fail_db(const char *operation, int ret)
{
fprintf(stderr, "%s failed: %s (%d)\n", operation, db_strerror(ret), ret);
exit(2);
}
static void
set_record(DBT *key, DBT *data, char *key_buf, char *data_buf, int number)
{
memset(key, 0, sizeof(*key));
memset(data, 0, sizeof(*data));
snprintf(key_buf, 16, "acct%04d", number);
snprintf(data_buf, 32, "balance=%d", 100 * number);
key->data = key_buf;
key->size = (u_int32_t)strlen(key_buf);
data->data = data_buf;
data->size = (u_int32_t)strlen(data_buf);
}
static void
produce_legacy(const char *path)
{
DB *db;
DBT key, data;
char key_buf[16], data_buf[32];
int i, ret;
if (access(path, F_OK) == 0) {
fprintf(stderr, "%s already exists\n", path);
exit(2);
}
if ((ret = db_create(&db, NULL, 0)) != 0)
fail_db("db_create", ret);
if ((ret = db->set_pagesize(db, PAGE_SIZE)) != 0) {
(void)db->close(db, 0);
fail_db("DB->set_pagesize", ret);
}
if ((ret = db->open(db, NULL, path, NULL, DB_HASH,
DB_CREATE, 0600)) != 0) {
(void)db->close(db, 0);
fail_db("DB->open", ret);
}
for (i = 0; i < ACCOUNT_COUNT; i++) {
set_record(&key, &data, key_buf, data_buf, i);
if ((ret = db->put(db, NULL, &key, &data, 0)) != 0)
fail_db("DB->put", ret);
}
if ((ret = db->close(db, 0)) != 0)
fail_db("DB->close", ret);
printf("producer_libdb=%s\n", db_version(NULL, NULL, NULL));
printf("producer_records=%d\n", ACCOUNT_COUNT);
}
#if DB_VERSION_MAJOR > 4 || \
(DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 6)
static int
byte_compare(DB *db, const DBT *left, const DBT *right)
{
size_t length;
int ret;
(void)db;
comparator_calls++;
length = left->size < right->size ? left->size : right->size;
ret = memcmp(left->data, right->data, length);
if (ret == 0)
ret = left->size < right->size ? -1 :
left->size > right->size ? 1 : 0;
if (ret == 0)
comparator_equalities++;
return ret;
}
static DB *
open_db(const char *path, int custom, u_int32_t flags)
{
DB *db;
int ret;
if ((ret = db_create(&db, NULL, 0)) != 0)
fail_db("db_create", ret);
if (custom && (ret = db->set_h_compare(db, byte_compare)) != 0) {
(void)db->close(db, 0);
fail_db("DB->set_h_compare", ret);
}
if ((ret = db->open(db, NULL, path, NULL, DB_HASH, flags, 0600)) != 0) {
(void)db->close(db, 0);
fail_db("DB->open", ret);
}
return db;
}
static void
fail_system(const char *operation)
{
perror(operation);
exit(2);
}
static const char *
page_type_name(unsigned char type)
{
switch (type) {
case P_HASHMETA:
return "P_HASHMETA";
case P_HASH_UNSORTED:
return "P_HASH_UNSORTED";
case P_HASH:
return "P_HASH";
default:
return NULL;
}
}
static int
count_unsorted_pages(const char *path, const char *label)
{
struct stat state;
unsigned char type;
const char *name;
off_t offset;
int fd, count = 0;
if (stat(path, &state) != 0)
fail_system("stat database");
if (state.st_size <= 0 || state.st_size % PAGE_SIZE != 0) {
fprintf(stderr, "database is not page aligned\n");
exit(2);
}
if ((fd = open(path, O_RDONLY)) < 0)
fail_system("open database");
printf("%s=", label);
for (offset = 0; offset < state.st_size; offset += PAGE_SIZE) {
if (pread(fd, &type, 1, offset + PAGE_TYPE_OFFSET) != 1)
fail_system("pread page type");
name = page_type_name(type);
if (name == NULL) {
fprintf(stderr, "%s: unknown page type %u at offset %lld\n",
label, (unsigned)type, (long long)offset);
(void)close(fd);
exit(2);
}
printf(" %s", name);
if (type == P_HASH_UNSORTED)
count++;
}
printf(" unsorted_pages=%d\n", count);
if (close(fd) != 0)
fail_system("close database");
return count;
}
static void
count_records(const char *path, int *records, int *target_records)
{
DB *db;
DBC *cursor;
DBT key, data;
int ret;
u_int32_t db_flags;
db = open_db(path, 0, DB_RDONLY);
if ((ret = db->get_flags(db, &db_flags)) != 0)
fail_db("DB->get_flags", ret);
printf("db_flags=0x%x DB_DUP=%s DB_DUPSORT=%s\n", db_flags,
(db_flags & DB_DUP) == 0 ? "absent" : "present",
(db_flags & DB_DUPSORT) == 0 ? "absent" : "present");
if ((db_flags & (DB_DUP | DB_DUPSORT)) != 0) {
fprintf(stderr, "database unexpectedly permits duplicates\n");
exit(2);
}
if ((ret = db->cursor(db, NULL, &cursor, 0)) != 0)
fail_db("DB->cursor", ret);
memset(&key, 0, sizeof(key));
memset(&data, 0, sizeof(data));
*records = *target_records = 0;
while ((ret = cursor->get(cursor, &key, &data, DB_NEXT)) == 0) {
(*records)++;
if (key.size == 8 && memcmp(key.data, "acct0000", 8) == 0)
(*target_records)++;
}
if (ret != DB_NOTFOUND)
fail_db("DBcursor->get", ret);
if ((ret = cursor->close(cursor)) != 0)
fail_db("DBC->close", ret);
if ((ret = db->close(db, 0)) != 0)
fail_db("DB->close", ret);
}
static int
lookup_existing(const char *path, int custom)
{
DB *db;
DBT key, data;
char key_buf[16], data_buf[32];
const char *lookup_state;
int ret, close_ret;
comparator_calls = comparator_equalities = 0;
db = open_db(path, custom, DB_RDONLY);
set_record(&key, &data, key_buf, data_buf, 0);
ret = db->get(db, NULL, &key, &data, 0);
if (ret == 0 && (data.size != 9 || memcmp(data.data, "balance=0", 9) != 0)) {
fprintf(stderr, "lookup returned an unexpected value\n");
exit(2);
}
close_ret = db->close(db, 0);
if (close_ret != 0)
fail_db("DB->close", close_ret);
if (ret == 0)
lookup_state = "present";
else if (ret == DB_NOTFOUND)
lookup_state = "absent";
else
lookup_state = "error";
if (custom)
printf("read_trigger DB->get=%d (%s) lookup=%s comparator_calls=%lu "
"comparator_equalities=%lu\n", ret,
ret == 0 ? "unexpected" : db_strerror(ret), lookup_state,
comparator_calls, comparator_equalities);
else
printf("read_control DB->get=%d (%s) %s comparator_calls=%lu "
"comparator_equalities=%lu\n", ret,
ret == 0 ? "success" : db_strerror(ret),
ret == 0 ? "value=balance=0" : "value=unavailable",
comparator_calls, comparator_equalities);
return ret;
}
static int
put_nooverwrite(const char *path, int custom, int *records,
int *target_records, int *post_unsorted_pages)
{
DB *db;
DBT key, data;
char key_buf[16], data_buf[32];
int ret, close_ret;
comparator_calls = comparator_equalities = 0;
db = open_db(path, custom, 0);
set_record(&key, &data, key_buf, data_buf, 0);
data.data = (void *)"balance=999999";
data.size = 14;
ret = db->put(db, NULL, &key, &data, DB_NOOVERWRITE);
close_ret = db->close(db, 0);
if (close_ret != 0)
fail_db("DB->close", close_ret);
*post_unsorted_pages = count_unsorted_pages(path, "post_put_page_types");
count_records(path, records, target_records);
printf("%s DB->put(DB_NOOVERWRITE)=%d (%s) comparator_calls=%lu "
"comparator_equalities=%lu records=%d target_key_records=%d\n",
custom ? "trigger" : "control", ret,
ret == 0 ? "success" : db_strerror(ret), comparator_calls,
comparator_equalities, *records, *target_records);
return ret;
}
#endif
int
main(int argc, char **argv)
{
const char *version;
int major, minor, patch;
#if DB_VERSION_MAJOR > 4 || \
(DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 6)
int ret, records, target_records;
int initial_unsorted_pages, post_unsorted_pages;
#endif
if (argc != 3 || (strcmp(argv[1], "produce") != 0 &&
strcmp(argv[1], "read-control") != 0 &&
strcmp(argv[1], "read-trigger") != 0 &&
strcmp(argv[1], "control") != 0 && strcmp(argv[1], "trigger") != 0)) {
fprintf(stderr, "usage: %s produce|read-control|read-trigger|control|trigger DATABASE\n", argv[0]);
return 2;
}
version = db_version(&major, &minor, &patch);
printf("library=%s\n", version);
if (strcmp(argv[1], "produce") == 0) {
if (major != 4 || minor != 5 || patch != 20)
return 2;
produce_legacy(argv[2]);
return 0;
}
#if DB_VERSION_MAJOR > 4 || \
(DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 6)
if (major != 5 || minor != 3 || patch != 34)
return 2;
if (strcmp(argv[1], "read-control") == 0 ||
strcmp(argv[1], "read-trigger") == 0) {
if (count_unsorted_pages(argv[2], "read_page_types") < 1)
return 2;
ret = lookup_existing(argv[2], strcmp(argv[1], "read-trigger") == 0);
if (strcmp(argv[1], "read-control") == 0) {
if (ret != 0)
return 1;
printf("read_control_result=present_key_retrieved\n");
} else {
if (ret != DB_NOTFOUND || comparator_equalities < 1)
return 1;
printf("read_trigger_result=false_not_found_reproduced\n");
}
return 0;
}
initial_unsorted_pages = count_unsorted_pages(argv[2], "initial_page_types");
if (initial_unsorted_pages < 1)
return 2;
ret = put_nooverwrite(argv[2], strcmp(argv[1], "trigger") == 0,
&records, &target_records, &post_unsorted_pages);
if (strcmp(argv[1], "control") == 0) {
if (ret != DB_KEYEXIST || records != ACCOUNT_COUNT ||
target_records != 1 || post_unsorted_pages != initial_unsorted_pages)
return 1;
printf("control_result=DB_KEYEXIST_and_no_duplicate\n");
} else {
if (ret != 0 || comparator_equalities < 1 ||
records != ACCOUNT_COUNT + 1 || target_records != 2 ||
post_unsorted_pages != initial_unsorted_pages - 1)
return 1;
printf("trigger_result=duplicate_logical_key_reproduced\n");
}
return 0;
#else
fprintf(stderr, "reader mode requires libdb 5.3.34\n");
return 2;
#endif
}
Summary
libdb 5.3.34 has a Hash lookup defect when a key is stored inline on a legacy
P_HASH_UNSORTEDpage and the application configures a custom comparator that preserves the database's historical comparison. Version 5.3.34 accepts this older page format without an explicit database upgrade, so the page can persist after an application moves to the newer library; opening the file alone does not trigger the defect because the comparator must be set explicitly. In__ham_getindex_unsorted, the inline-key custom-comparator branch calls the comparator but fails to store its zero result, so it reports an equal stored key as absent. Consequently,DB->getreturnsDB_NOTFOUNDfor an existing key, andDB->put(..., DB_NOOVERWRITE)can return success and persist a duplicate even though duplicates are disabled.Environment
Berkeley DB 5.3.34: (August 3, 2026), commitc4811dc871e313033993e95baa5b6525057c5911Berkeley DB 4.5.20: (September 20, 2006), tagv4.5.20, commitc4abd8bf6266d112e79f1544a0277337f378e36bsource/build-issue:../dist/configure CFLAGS='-O2 -g'P_HASH_UNSORTED)DB->get;DB->putwithDB_NOOVERWRITESteps to reproduce
DB->open, callDB->set_h_comparewith thebyte_comparecallback shown below.DB->upgradeordb_upgrade. The data page remains a legacy page (P_HASH_UNSORTED).P_HASH_UNSORTED, the application callsDB->getorDB->put(..., DB_NOOVERWRITE)for the stored key.DB->set_h_compare. Both read cases use the unchanged producer file. Each write case starts from a separate copy.Complete release reproduction
Compile the following source against 4.5.20 as the producer and against 5.3.34 as the reader and writer. Save it as
repro-native.c.Run the reproduction in a fresh directory that contains
repro-native.c:Expected result
The 4.6 upgrade guide describes the change from unsorted to sorted Hash pages as backward-compatible:
Version 5.3.34 accepts the tested 4.5.20 Hash file and retains a lookup path for
P_HASH_UNSORTEDpages.Hash comparators were added in Berkeley DB 4.6, so the 4.5.20 producer used the built-in byte-by-byte comparison. For an existing database,
DB->set_h_comparerequires a comparator that implements the comparison used when the database was created (dbset_h_compare.md, lines 16-22).The
byte_comparecallback in the reproduction meets that requirement for every key used. It produces the same ordering and equality as the built-in comparison. Under the callback contract, a return value of 0 means that the search key and stored key are equal (lines 38-44).DB->getreturnsDB_NOTFOUNDonly if the specified key is absent (dbget.md, line 34).DB->getmust therefore retrieve the stored record.DB_NOOVERWRITEmust returnDB_KEYEXISTwithout inserting another record when the primary key already exists (dbput.md, lines 56-62).Actual result
The 5.3.34 release build produced these results with the database created by 4.5.20:
DB->getreturns 0 and retrieves the existing valueDB->getreturnsDB_NOTFOUNDDB_NOOVERWRITEreturnsDB_KEYEXIST; one record has the keyDB_NOOVERWRITEreturns 0; two records have identical key bytes after close and reopenThe read cases leave the producer file unchanged, and the write control leaves its copy byte-identical to the baseline. The write trigger changes the affected page from
P_HASH_UNSORTEDto the currentP_HASHtype and stores the second record.DB->get_flagsreports neitherDB_DUPnorDB_DUPSORT.Analysis
Root cause
__ham_getindexsends aP_HASH_UNSORTEDpage to__ham_getindex_unsorted. That function initializesresto 1 (line 666).H_KEYDATA), the custom-comparator branch usesh_compareonly to reject a nonzero result (lines 684-689). Whenh_comparereturns 0, the branch does not assign 0 tores, soresretains its nonzero value.__ham_getindex_unsortedsetsmatchto 0 only whenresis 0. It therefore reports this equal key as not found (lines 704-709). The built-in and sorted-page branches do assign their comparison results (lines 690-693, lines 814-824).__ham_lookupfor aDB_SEToperation (hash.c, lines 529-534). Because no entry is marked as found,__ham_lookupreturnsDB_NOTFOUND;DB->getreturns that cursor result (db_iface.c, lines 802-815).DB_NOOVERWRITE,__hamc_putreturnsDB_KEYEXISTonly after a successful lookup. The falseDB_NOTFOUNDinstead sends the operation to__ham_add_el. In the reproduced write,__ham_add_elsorts the affectedP_HASH_UNSORTEDpage. The sort invalidates the saved insertion position, so__ham_add_elsearches again to calculate a new index. The second search finds the key, but noDB_NOOVERWRITEcheck is repeated.__ham_add_elthen calls__ham_insertpairwith the new index (sorting; second search and insertion).Impact and scope
The false
DB_NOTFOUNDcan make an application run its missing-record handling for a stored key. On writes, it defeatsDB_NOOVERWRITEas a create-if-absent guard.DB->putreturns 0 and stores a second byte-identical key although duplicate keys are disabled.The faulty branch requires all of the following:
P_HASH_UNSORTED;A new Hash handle initializes
h_compareto NULL, so opening the older file alone does not trigger this defect.The reproduction uses an unmodified, native-byte-order file produced by Berkeley DB 4.5.20. It uses equal-length keys and nontransactional calls without a database environment. Equal-length off-page keys take a separate branch that passes the comparison result through
__db_moff(lines 672-681); the reproduction does not exercise that branch.After libdb sorts the affected page, later lookups no longer use the faulty
P_HASH_UNSORTEDbranch. For the tested native-byte-order version-8 file,DB->upgradeselects the 4.6 Hash conversion and database pass.DB->upgradeuses a page-type table that sends eachP_HASH_UNSORTEDpage to the sorting callback. If libdb 5.3.34 inserts on that page before the reportedDB->getorDB->put(..., DB_NOOVERWRITE)call, the insertion also sorts the page (hash_page.c, lines 2599-2602).In the reproduced write, sorting occurs only after the false miss. The sort prevents later misses through this branch, but it does not remove the inserted duplicate.