Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix use-after-free in unit tests #686

Merged
merged 5 commits into from
Aug 21, 2024

Conversation

cole-miller
Copy link
Contributor

@cole-miller cole-miller commented Aug 16, 2024

Free the pool's pool_impl_t containing the outq_async handle only after the callback passed to uv_close has fired, in line with libuv rules, and in contrast to the original code, which could free this allocation while libuv was still holding a pointer into it. This UAF showed up in CI runs for #682 but is hard to reproduce otherwise.

Note that dqlite as used in production is not affected by this bug since the thread pool is not even created in that case.

Signed-off-by: Cole Miller [email protected]

@cole-miller
Copy link
Contributor Author

I suspect this was behind the elusive hangs we were seeing in CI as well...

@cole-miller cole-miller requested a review from just-now August 16, 2024 20:50
@cole-miller
Copy link
Contributor Author

Need to fix pool setup/teardown in server.c as well.

@cole-miller cole-miller marked this pull request as draft August 16, 2024 21:03
@cole-miller cole-miller marked this pull request as ready for review August 16, 2024 21:28
This prevents a use-after-free of the pool's async handle.

Signed-off-by: Cole Miller <[email protected]>
The loop must run to completion before calling pool_fini.

Signed-off-by: Cole Miller <[email protected]>
It's unsound to uv_close the async handle and then immediately free the
pool_impl_t, because libuv will still hold a pointer to the handle.
Avoid the need to do this by calling uv_async_init last, so there's no
need to roll it back when a later operation fails.

Signed-off-by: Cole Miller <[email protected]>
@cole-miller cole-miller changed the title Fix use-after-free in the new thread pool Fix use-after-free in unit tests Aug 20, 2024
@cole-miller
Copy link
Contributor Author

cole-miller commented Aug 20, 2024

I discussed this offline with @just-now, who pointed out that the pool was designed based on the assumption that pool_fini would not be called until the associated event loop had been run to completion. This makes it sound to free the pool_impl_t in pool_fini. I've updated the tests to use pool_fini as intended and documented the thread pool's closing sequence.

However, there is a bigger issue: gateway__leader_close is unsound when using the new thread pool, because it invokes the request callbacks in a way that's not synchronized with operations running on the pool that use the same resources, in particular prepared statements. So I've just disabled this test for now when DQLITE_NEXT is set, with a note about the race condition.

Copy link

codecov bot commented Aug 20, 2024

Codecov Report

Attention: Patch coverage is 42.85714% with 4 lines in your changes missing coverage. Please review.

Project coverage is 77.87%. Comparing base (2d0d71b) to head (9387b77).
Report is 7 commits behind head on master.

Files Patch % Lines
src/lib/threadpool.c 20.00% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #686      +/-   ##
==========================================
- Coverage   77.87%   77.87%   -0.01%     
==========================================
  Files         197      197              
  Lines       27953    27955       +2     
  Branches     5534     5532       -2     
==========================================
+ Hits        21768    21769       +1     
+ Misses       4346     4345       -1     
- Partials     1839     1841       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

There's a race here that we haven't figured out how to address.

Signed-off-by: Cole Miller <[email protected]>
Comment on lines +112 to +114
* released, until pool_fini is called. Before calling pool_fini, the event loop
* that was used to create this pool must be run to completion (that is, until
* uv_run returns 0).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check my understanding, here we are exploiting the fact that (source):

Handles that wrap file descriptors are closed immediately

So calling uv_close will close the handle and we can free it immediately after. However, we still have to run the event loop to completion prior to doing that because the handles might be in use resulting in UAF.

Is that right?

Copy link
Contributor Author

@cole-miller cole-miller Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So calling uv_close will close the handle and we can free it immediately after.

No, it won't be closed immediately (uv_async_t doesn't wrap an fd). It'll be closed asynchronously, but we don't free the allocation until after the event loop has exited, which implies that all handles including the pool's async handle have finished closing.

@cole-miller cole-miller merged commit 6035a35 into canonical:master Aug 21, 2024
16 of 18 checks passed
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.

3 participants