-
Notifications
You must be signed in to change notification settings - Fork 46
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
Consider detecting use of Connection after close #86
Comments
@Shimuuar thanks for the report. I will try to narrow it down and add additional checks. |
Probably should go into direct-sqlite. I'd maybe just add a "closed" flag in the connection handle, and check that before closing the real handle. I don't remember how the connection datatype looks like in direct-sqlite, maybe it needs to wrap the actual C sqlite handle. |
Connection is just a few layers of newtypes over bare Challenge in adding "closed" flag I think is adding it in the way which doesn't allow for race conditions and is fast at the same time. |
Well, you're supposed to call Sure you'll have one indirection to get to the connection, but that really cannot be a problem, given how much sqlite3 calls themselves cost. Re races: not sure about this. Seems like normally you wouldn't have two threadds fighting for their turn to close a connection. In general, the only stable way to use SQLite3 from multiple threads was to anyway effectively single-thread access with an |
Every database operation should check whether connection is already closed or not. Otherwise it will start working with already free chunk of memory and will wreak malloc's data structures. Original problem arose because thread was working with database in short window between closing the DB and killing of thread |
Good point. Could the connection have a mutable reference like an IORef maybe that holds the connection, then it'd be set to Nothing on close? Then every function using the connection would have to first acquire the real connection using some getter function that raises an error if the connection was inexistent? It might in fact make sense to have this in sqlite-simple to keep direct-sqlite as lean as possible. I don't see the IORef as a perf problem either. It's a simple check. But it'd be a large, though mechanical, change to sqlite-simple. Wouldn't probably change the public API at all. |
IORef won't give you thread safety. If connection is used in multiple threads it's still possible to get use after free. For example two threads racing
First thread check flag before it set to "closed" state, but runs query after database is closed. To provide thread safety |
Yes, I know -- I was just throwing the general pattern here. Another consideration is what is the right way to use direct-sqlite (and SQLite3!) from multiple threads. In practice you need to protect your connection with an MVar anyway, or you'll hit those SQLITE_BUSY errors when one or more threads write at concurrently on the same connection. So probably the MVar shouldn't go into direct-sqlite. But it might be beneficial to have a fix for the "closed/use after free" problem even in a thread unsafe manner. Because that bug can exist even in single-thread cases. As a retired author/maintainer of sqlite-simple, I'm not in a position to make a call about MVar. I'd probably just make sure it's well documented and provide guidelines on what's a safe way to use this API from multiple threads. This, as opposed to MVaring the connection and making a large, breaking API change to sqlite-simple. There are a lot of users of sqlite-simple and many probably using sqlite in a single-threaded fashion. True concurrent database users are quite likely using something like Postgresql. |
@nurpax thanks for the hints. I haven't got into details of that, want to reproduce case first, and find if that a typical issue. Are you suggesting it's better to improve documentation on how to use that rather than drastically change API? |
I think it's possible to make sqlite-simple to track closed database and thread being thread safe without changing API at all. Here is general idea: newtype Connection = Connection (MVar (Maybe Base.Database))
open :: String -> IO Connection
open fname = do
c <- Base.open (T.pack fname)
Connection <$> newMVar (Just c)
close :: Connection -> IO ()
close (Connection mvar) = modifyMVar_ mvar $ \case
Nothing -> return Nothing
Just c -> Nothing <$ Base.close
execute :: (ToRow q) => Connection -> Query -> q -> IO ()
execute (Connection mvar)template qs = withMvar $ \case
Nothing -> throwIO DatabaseAlreadyClosed
Just c ->
withStatementParams conn template qs $ \(Statement stmt) ->
void . Base.step $ stmt Such change is boilerplate heavy and requires to think when lock is taken to avoid deadlock. But it doesn't change API |
SQLite's internal
sqlite3
struct is allocated by malloc and freed after database is closed. So any attempt to use database after it's closed results in use after free, malloc's data structures corruption, segfaults and all kind of problems. It would be nice to add some check to convert any such attempt to exceptions.I ran into this problem when test suite (thankfully not a production) crashed occasionally. It turned out that due to race condition during cleanup it was possible to use database right after it was closed. It took a lot of time to figure out what was going on
The text was updated successfully, but these errors were encountered: