Skip to content

fix: prevent use-after-free when GC finalizes NodeQueryResult#10

Merged
adsharma merged 2 commits intomainfrom
fix_use_after_free
Apr 16, 2026
Merged

fix: prevent use-after-free when GC finalizes NodeQueryResult#10
adsharma merged 2 commits intomainfrom
fix_use_after_free

Conversation

@adsharma
Copy link
Copy Markdown
Contributor

Fixes: #8

NodeQueryResult held a unique_ptr which internally owns a FactorizedTable that accesses database-managed memory (buffer pool, allocators) during destruction. If the Database was explicitly destroyed via db.closeSync() before the GC finalizer for NodeQueryResult ran, the FactorizedTable destructor would access freed memory, crashing the process.

This manifested whenever query results were not explicitly closed before closing the connection and database -- a common and reasonable usage pattern.

Fix: NodeQueryResult now holds a shared_ptr alongside its QueryResult. The database ref is acquired in AdoptQueryResult and released last in Close(), after ownedQueryResult is reset. This guarantees the Database cannot be freed until every NodeQueryResult that references it has been destroyed, regardless of GC timing.

To thread the shared_ptr through:

  • NodeConnection retains its database shared_ptr after InitCppConnection() instead of dropping it immediately (the drop is moved to Close())
  • Both async workers (ConnectionQueryAsyncWorker, ConnectionExecuteAsyncWorker) carry a shared_ptr and forward it to AdoptQueryResult
  • QuerySync and ExecuteSync pass the connection's database ref directly
  • NewInstance and GetNextQueryResultSync/Async propagate the ref to child results created when iterating multi-statement query chains

Adds regression test: discards query results without closing them, then calls conn.closeSync() and db.closeSync(), verifying the process does not crash when the GC finalizer later runs.

…atabase close

NodeQueryResult held a unique_ptr<MaterializedQueryResult> which internally
owns a FactorizedTable that accesses database-managed memory (buffer pool,
allocators) during destruction. If the Database was explicitly destroyed via
db.closeSync() before the GC finalizer for NodeQueryResult ran, the
FactorizedTable destructor would access freed memory, crashing the process.

This manifested whenever query results were not explicitly closed before
closing the connection and database -- a common and reasonable usage pattern.

Fix: NodeQueryResult now holds a shared_ptr<Database> alongside its
QueryResult. The database ref is acquired in AdoptQueryResult and released
last in Close(), after ownedQueryResult is reset. This guarantees the
Database cannot be freed until every NodeQueryResult that references it has
been destroyed, regardless of GC timing.

To thread the shared_ptr through:
- NodeConnection retains its database shared_ptr after InitCppConnection()
  instead of dropping it immediately (the drop is moved to Close())
- Both async workers (ConnectionQueryAsyncWorker,
  ConnectionExecuteAsyncWorker) carry a shared_ptr<Database> and forward it
  to AdoptQueryResult
- QuerySync and ExecuteSync pass the connection's database ref directly
- NewInstance and GetNextQueryResultSync/Async propagate the ref to child
  results created when iterating multi-statement query chains

Adds regression test: discards query results without closing them, then
calls conn.closeSync() and db.closeSync(), verifying the process does not
crash when the GC finalizer later runs.
NodeConnection now holds a shared_ptr<Database> to keep the database alive
until all NodeQueryResults are GC'd. As a side effect, db.closeSync() no
longer immediately invalidates the connection -- the database stays alive
until the connection also releases its ref. Drop the assertions that querying
and iterating throw after db.closeSync(); just verify the reverse close order
doesn't crash.
@adsharma adsharma merged commit c4aff2d into main Apr 16, 2026
4 checks passed
@adsharma adsharma deleted the fix_use_after_free branch April 16, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

close() / closeSync() causes SIGSEGV or process hang after query execution on file-based databases

1 participant