https://bugs.openldap.org/show_bug.cgi?id=10462
Issue ID: 10462
Summary: MDB_NEXT/MDB_NEXT_NODUP/MDB_PREV_NODUP on cursor
returns re-inserted key after mdb_del+mdb_put empties
and repopulates DUPSORT db
Product: LMDB
Version: 0.9.35
Hardware: x86_64
OS: Linux
Status: UNCONFIRMED
Keywords: needs_review
Severity: normal
Priority: ---
Component: liblmdb
Assignee: [email protected]
Reporter: [email protected]
Target Milestone: ---
Created attachment 1120
--> https://bugs.openldap.org/attachment.cgi?id=1120&action=edit
C file that reproduces problem
Overview:
When a cursor is positioned on the only key in a MDB_DUPSORT database, and that
key is deleted via mdb_del() then re-inserted via
mdb_put() (both via the transaction API, not the cursor), subsequent
mdb_cursor_get() with MDB_NEXT, MDB_NEXT_NODUP, or
MDB_PREV_NODUP incorrectly returns the re-inserted key instead of MDB_NOTFOUND.
This causes infinite loops in cursor traversal
code.
The bug does not occur when there are multiple unique keys in the database —
only when the delete empties the B-tree entirely.
Steps to Reproduce:
1. Create a MDB_DUPSORT database
2. Insert a single key with one or more dup values
3. Open a cursor and position it at the key via MDB_FIRST
4. Delete the key via mdb_del() (not mdb_cursor_del())
5. Re-insert the same key via mdb_put()
6. Call mdb_cursor_get() with MDB_NEXT_NODUP
Reproducer program attached. (build with cc -o repro repro.c mdb.c midl.c -I.
-lpthread):
Actual Results:
mdb_cursor_get(MDB_NEXT_NODUP) returns MDB_SUCCESS and the cursor is positioned
at the re-inserted key. In a traversal loop, this causes an infinite loop.
Expected Results:
mdb_cursor_get(MDB_NEXT_NODUP) should return MDB_NOTFOUND since there are no
more unique keys after the cursor's position.
Root Cause:
When mdb_del() removes the only key, mdb_rebalance() empties the B-tree and
clears C_INITIALIZED on all cursors (mdb.c line ~8407). The mdb_cursor_del0()
function had already set C_DEL on other cursors at the same position (line
~8555). After mdb_put() re-inserts data, mdb_cursor_next() sees !C_INITIALIZED
and falls through to mdb_cursor_first() (line 5967-5968), ignoring the fact
that C_DEL indicates the cursor's entry was deleted. The same issue affects
mdb_cursor_prev() (line 6049-6053).
Proposed Fix:
In mdb_cursor_next() and mdb_cursor_prev(), when C_INITIALIZED is not set,
check for C_DEL first. If C_DEL is set, the cursor was positioned at a deleted
entry and should not fall through to first/last:
// mdb_cursor_next, line ~5967:
if (!(mc->mc_flags & C_INITIALIZED)) {
if (mc->mc_flags & C_DEL)
return MDB_NOTFOUND;
return mdb_cursor_first(mc, key, data);
}
// mdb_cursor_prev, line ~6049:
if (!(mc->mc_flags & C_INITIALIZED)) {
if (mc->mc_flags & C_DEL)
return MDB_NOTFOUND;
rc = mdb_cursor_last(mc, key, data);
Build Date & Platform: LMDB 0.9.35, Linux 6.6.87, x86_64. Also confirmed on
FreeBSD 14.3 with LMDB 0.9.33.
--
You are receiving this mail because:
You are on the CC list for the issue.