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.