From 753b2270fdfc8df36a23c82e2e2707c7357de955 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=9B=D0=B5=D0=BE=D0=BD=D0=B8=D0=B4=20=D0=AE=D1=80=D1=8C?=
 =?UTF-8?q?=D0=B5=D0=B2=20=28Leonid=20Yuriev=29?= <leo@yuriev.ru>
Date: Thu, 20 Mar 2025 01:14:34 +0300
Subject: [PATCH] =?UTF-8?q?mdbx:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?=
 =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20`mdbx=5Fcursor=5Fclose2()`=20=D0=B2?=
 =?UTF-8?q?=20API=20(backport).?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 mdbx.h           | 30 +++++++++++++++-
 mdbx.h++         |  5 ++-
 src/api-cursor.c | 91 ++++++++++++++++++++++++++++++------------------
 src/cursor.c     |  4 +--
 src/mdbx.c++     |  9 -----
 5 files changed, 93 insertions(+), 46 deletions(-)

diff --git a/mdbx.h b/mdbx.h
index 559c664b..e8e5748b 100644
--- a/mdbx.h
+++ b/mdbx.h
@@ -5210,12 +5210,17 @@ LIBMDBX_API int mdbx_cursor_reset(MDBX_cursor *cursor);
  * \retval MDBX_EINVAL  An invalid parameter was specified. */
 LIBMDBX_API int mdbx_cursor_open(MDBX_txn *txn, MDBX_dbi dbi, MDBX_cursor **cursor);
 
-/** \brief Close a cursor handle.
+/** \brief Closes a cursor handle without returning error code.
  * \ingroup c_cursors
  *
  * The cursor handle will be freed and must not be used again after this call,
  * but its transaction may still be live.
  *
+ * This function returns `void` but panic in case of error. Use \ref mdbx_cursor_close2()
+ * if you need to receive an error code instead of an app crash.
+ *
+ * \see mdbx_cursor_close2
+ *
  * \note In contrast to LMDB, the MDBX required that any opened cursors can be
  * reused and must be freed explicitly, regardless ones was opened in a
  * read-only or write transaction. The REASON for this is eliminates ambiguity
@@ -5226,6 +5231,29 @@ LIBMDBX_API int mdbx_cursor_open(MDBX_txn *txn, MDBX_dbi dbi, MDBX_cursor **curs
  *                     or \ref mdbx_cursor_create(). */
 LIBMDBX_API void mdbx_cursor_close(MDBX_cursor *cursor);
 
+/** \brief Closes a cursor handle with returning error code.
+ * \ingroup c_cursors
+ *
+ * The cursor handle will be freed and must not be used again after this call,
+ * but its transaction may still be live.
+ *
+ * \see mdbx_cursor_close
+ *
+ * \note In contrast to LMDB, the MDBX required that any opened cursors can be
+ * reused and must be freed explicitly, regardless ones was opened in a
+ * read-only or write transaction. The REASON for this is eliminates ambiguity
+ * which helps to avoid errors such as: use-after-free, double-free, i.e.
+ * memory corruption and segfaults.
+ *
+ * \param [in] cursor  A cursor handle returned by \ref mdbx_cursor_open()
+ *                     or \ref mdbx_cursor_create().
+ * \returns A non-zero error value on failure and 0 on success,
+ *          some possible errors are:
+ * \retval MDBX_THREAD_MISMATCH  Given transaction is not owned
+ *                               by current thread.
+ * \retval MDBX_EINVAL  An invalid parameter was specified. */
+LIBMDBX_API int mdbx_cursor_close2(MDBX_cursor *cursor);
+
 /** \brief Unbind or closes all cursors of a given transaction.
  * \ingroup c_cursors
  *
diff --git a/mdbx.h++ b/mdbx.h++
index e993f17f..3b6e6239 100644
--- a/mdbx.h++
+++ b/mdbx.h++
@@ -4545,7 +4545,10 @@ public:
   }
 
   /// \brief Explicitly closes the cursor.
-  void close();
+  inline void close() {
+    error::success_or_throw(::mdbx_cursor_close2(handle_));
+    handle_ = nullptr;
+  }
 
   cursor_managed(cursor_managed &&) = default;
   cursor_managed &operator=(cursor_managed &&other) noexcept {
diff --git a/src/api-cursor.c b/src/api-cursor.c
index cfe935f1..d86bc34e 100644
--- a/src/api-cursor.c
+++ b/src/api-cursor.c
@@ -93,6 +93,10 @@ int mdbx_cursor_unbind(MDBX_cursor *mc) {
     /* TODO: реализовать при переходе на двусвязный список курсоров */
     return LOG_IFERR(MDBX_EINVAL);
 
+  int rc = check_txn(mc->txn, MDBX_TXN_FINISHED | MDBX_TXN_HAS_CHILD);
+  if (unlikely(rc != MDBX_SUCCESS))
+    return LOG_IFERR(rc);
+
   if (unlikely(!mc->txn || mc->txn->signature != txn_signature)) {
     ERROR("Wrong cursor's transaction %p 0x%x", __Wpedantic_format_voidptr(mc->txn), mc->txn ? mc->txn->signature : 0);
     return LOG_IFERR(MDBX_PROBLEM);
@@ -137,42 +141,63 @@ int mdbx_cursor_open(MDBX_txn *txn, MDBX_dbi dbi, MDBX_cursor **ret) {
   return MDBX_SUCCESS;
 }
 
-void mdbx_cursor_close(MDBX_cursor *mc) {
-  if (likely(mc)) {
-    ENSURE(nullptr, mc->signature == cur_signature_live || mc->signature == cur_signature_ready4dispose);
-    MDBX_txn *const txn = mc->txn;
-    if (!mc->backup) {
-      mc->txn = nullptr;
-      /* Unlink from txn, if tracked. */
-      if (mc->next != mc) {
-        ENSURE(txn->env, check_txn(txn, 0) == MDBX_SUCCESS);
-        const size_t dbi = (kvx_t *)mc->clc - txn->env->kvs;
-        tASSERT(txn, dbi < txn->n_dbi);
-        if (dbi < txn->n_dbi) {
-          MDBX_cursor **prev = &txn->cursors[dbi];
-          while (*prev) {
-            ENSURE(txn->env, (*prev)->signature == cur_signature_live || (*prev)->signature == cur_signature_wait4eot);
-            if (*prev == mc)
-              break;
-            prev = &(*prev)->next;
-          }
-          tASSERT(txn, *prev == mc);
-          *prev = mc->next;
-        }
-        mc->next = mc;
-      }
-      mc->signature = 0;
-      osal_free(mc);
-    } else {
-      /* Cursor closed before nested txn ends */
-      tASSERT(txn, mc->signature == cur_signature_live);
-      ENSURE(txn->env, check_txn_rw(txn, 0) == MDBX_SUCCESS);
-      be_poor(mc);
-      mc->signature = cur_signature_wait4eot;
-    }
+void mdbx_cursor_close(MDBX_cursor *cursor) {
+  if (likely(cursor)) {
+    int err = mdbx_cursor_close2(cursor);
+    if (unlikely(err != MDBX_SUCCESS))
+      mdbx_panic("%s:%d error %d (%s) while closing cursor", __func__, __LINE__, err, mdbx_liberr2str(err));
   }
 }
 
+int mdbx_cursor_close2(MDBX_cursor *mc) {
+  if (unlikely(!mc))
+    return LOG_IFERR(MDBX_EINVAL);
+
+  if (mc->signature == cur_signature_ready4dispose) {
+    if (unlikely(mc->txn || mc->backup))
+      return LOG_IFERR(MDBX_PANIC);
+    cursor_drown((cursor_couple_t *)mc);
+    mc->signature = 0;
+    osal_free(mc);
+    return MDBX_SUCCESS;
+  }
+
+  if (unlikely(mc->signature != cur_signature_live))
+    return LOG_IFERR(MDBX_EBADSIGN);
+
+  MDBX_txn *const txn = mc->txn;
+  int rc = check_txn(txn, MDBX_TXN_FINISHED);
+  if (unlikely(rc != MDBX_SUCCESS))
+    return LOG_IFERR(rc);
+
+  if (mc->backup) {
+    /* Cursor closed before nested txn ends */
+    cursor_reset((cursor_couple_t *)mc);
+    mc->signature = cur_signature_wait4eot;
+    return MDBX_SUCCESS;
+  }
+
+  if (mc->next != mc) {
+    const size_t dbi = cursor_dbi(mc);
+    cASSERT(mc, dbi < mc->txn->n_dbi);
+    cASSERT(mc, &mc->txn->env->kvs[dbi].clc == mc->clc);
+    if (likely(dbi < txn->n_dbi)) {
+      MDBX_cursor **prev = &txn->cursors[dbi];
+      while (/* *prev && */ *prev != mc) {
+        ENSURE(txn->env, (*prev)->signature == cur_signature_live || (*prev)->signature == cur_signature_wait4eot);
+        prev = &(*prev)->next;
+      }
+      tASSERT(txn, *prev == mc);
+      *prev = mc->next;
+    }
+    mc->next = mc;
+  }
+  cursor_drown((cursor_couple_t *)mc);
+  mc->signature = 0;
+  osal_free(mc);
+  return MDBX_SUCCESS;
+}
+
 int mdbx_cursor_copy(const MDBX_cursor *src, MDBX_cursor *dest) {
   if (unlikely(!src))
     return LOG_IFERR(MDBX_EINVAL);
diff --git a/src/cursor.c b/src/cursor.c
index 73da6a8b..bb6f4e61 100644
--- a/src/cursor.c
+++ b/src/cursor.c
@@ -252,9 +252,9 @@ MDBX_cursor *cursor_eot(MDBX_cursor *mc, MDBX_txn *txn, const bool merge) {
     osal_free(bk);
   } else {
     ENSURE(mc->txn->env, stage == cur_signature_live);
-    cursor_drown((cursor_couple_t *)mc);
-    mc->next = mc;
     mc->signature = cur_signature_ready4dispose /* Cursor may be reused */;
+    mc->next = mc;
+    cursor_drown((cursor_couple_t *)mc);
   }
   return next;
 }
diff --git a/src/mdbx.c++ b/src/mdbx.c++
index d891b654..63f56190 100644
--- a/src/mdbx.c++
+++ b/src/mdbx.c++
@@ -1590,15 +1590,6 @@ __cold bool txn::rename_map(const ::std::string &old_name, const ::std::string &
 
 //------------------------------------------------------------------------------
 
-void cursor_managed::close() {
-  if (MDBX_UNLIKELY(!handle_))
-    MDBX_CXX20_UNLIKELY error::throw_exception(MDBX_EINVAL);
-  ::mdbx_cursor_close(handle_);
-  handle_ = nullptr;
-}
-
-//------------------------------------------------------------------------------
-
 __cold ::std::ostream &operator<<(::std::ostream &out, const slice &it) {
   out << "{";
   if (!it.is_valid())