ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Major - P3
    • WT12.0.0, 9.1.0-rc0
    • Affects Version/s: None
    • Component/s: Cursors
    • None
    • Storage Engines - Transactions
    • 266.014
    • SE Transactions - 2026-10-23
    • 1

      Background

      Found while evaluating where to put the blind_remove ENOTSUP check for WT-18922. WT-18922 keeps that check in __session_open_cursor, so it does not depend on this fix. Related: WT-18942.

      Problem

      The error path in __wt_cursor_init (src/cursor/cur_std.c) always frees cursor->internal_uri:

      err:
          if (ret != 0) {
              __wt_free(session, cursor->internal_uri);
              cursor->internal_uri = NULL;
          }
          return (ret);
      

      But _wt_cursor_init only allocates internal_uri when it is NULL on entry. Most cursor types set it before calling wt_cursor_init, pointing at a string owned by something else, and clear it in their own close function so _wt_cursor_close does not free it:

      Cursor Borrowed from Cleared in
      file btree->dhandle->name, in __curfile_create __curfile_close ("The URI is owned by the btree handle.")
      layered the layered dhandle's name, in __wt_clayered_open __clayered_close
      table table->iface.name, in __wt_curtable_open __curtable_close
      index idx->name, in __wt_curindex_open __curindex_close
      dump the child cursor's internal_uri, in __wti_curdump_create __curdump_close

      Impact

      If _wt_cursor_init fails for one of these cursors, it frees a name that a live dhandle, table or index still owns. Lookups and other cursors that share the name then read freed memory, and the owner frees it again when it is destroyed (_conn_dhandle_destroy), which is a double free. In release builds this is silent heap corruption.

      Reachability

      No known trigger today other than allocation failure. After the URI is set, the only step that can fail is _cursor_reuse_or_init. Its remaining failure modes are lookups of config that was already validated at API entry, and memory allocation in wti_curdump_create. Any new check in _cursor_reuse_or_init that can fail makes this triggerable from user input.

      Reproduction

      Move the blind_remove ENOTSUP check from _session_open_cursor into _cursor_reuse_or_init, then run test_layered_cursor31 on an ASan build. The plain-table scenario of test_open_cursor reports heap-use-after-free:

      • allocated in __wt_conn_dhandle_alloc (the dhandle name);
      • freed in _wt_cursor_init <- curfile_create <- wt_curfile_open <- _wt_curtable_open (simple-table path);
      • used in __session_close_cursors, through another cursor's internal_uri that points at the same name.

      Proposed fix

      Free only what __wt_cursor_init allocated:

      owns_internal_uri = false;
      if (cursor->internal_uri == NULL) {
          WT_ASSERT(session, uri != NULL);
          WT_RET(__wt_strdup(session, uri, &cursor->internal_uri));
          owns_internal_uri = true;
      }
      ...
      err:
          if (owns_internal_uri)
              __wt_free(session, cursor->internal_uri);
          return (ret);
      

      This keeps the leak fix from WT-15544 for cursor types that do not set internal_uri themselves. With this change, plus the fix for WT-18942, the reproduction above passes under ASan. So do 406 tests across the cursor-cache, dump, sweep and layered test files.

      Testing

      Nothing a user does can trigger this today. A regression test needs a forced failure inside __wt_cursor_init after a borrowed URI has been set (for example a failpoint or a Catch2 test), run under ASan.

            Assignee:
            Zunyi Liu
            Reporter:
            Zunyi Liu
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: