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.

Reply via email to