Avoid async exceptions footgun in shelly'#235
Conversation
This commit uses `catch` and `catches` from the `safe-exceptions` library in a few places, notably in `catches_sh`, which is run as part of `shelly'`. Without this commit, if a `Sh` computation were being run as part of an async computation (say, a `snap` web handler) if we had an async exception being thrown by the main handler, it would be caught by `catches_sh` by rethrown as a sync exception via `ReThrownException`, meaning that if the caller of `shelly` had an exception handler like `try` _even_ if it came from the `safe-exceptions` library, this would be now be exercised because the exception turned into a synchronous one. This is especially problematic because it hides the real culprit; a user might be misled thinking this was an exception coming from the guts of the `shelly` computation, whereas it was not.
Uhm, CI seems to be unrelated to my changes, at least from a quick glance? |
Yes, Haskell CI for 9.14 is currently not possible: |
|
@adinapoli Thanks for the PR. |
@andreasabel of course :) Easter holidays might get in the way, but it will come. |
|
@andreasabel As promised, here is the reproducer. I have a full, dead-simple cabal project stashed away if you want me to put it online somewhere for you to experiment. Either way, consider the following: {-# LANGUAGE NumericUnderscores #-}
module Main where
import Control.Concurrent (threadDelay)
import qualified Control.Exception.Safe as Safe
import System.Timeout (timeout)
import Shelly
doWork :: IO (Either String ())
doWork = do
res <- Safe.tryAny $ shelly $ silently $ liftIO $ threadDelay 5_000_000
case res of
Left err -> pure $ Left $ Safe.displayException err
Right _ -> pure $ Right ()
main :: IO ()
main = do
-- Run a long-lived action in a separate thread, simulating a web
-- request handler that shells out during processing.
threadResult <- timeout 1_000_000 doWork
case threadResult of
Nothing -> putStrLn $ "OK: timeout hit! shelly didn't catch the async Timeout exception."
Just res -> case res of
Left err -> putStrLn $ "BUG: shelly caught async exception: " <> err
Right () -> putStrLn $ "OK: we returned. (never exercised in this reproducer!)"This replicates what happens in a typical backend server application (or, at least, what happens is my server) we have a main thread spawning subthreads to serve requests, and we have a timeout that throws an async exception to kill the thread to avoid Slowloris attacks. Here we are conveniently using the The conjured scenario is simple: we spawn an async action and we want to wait for its result and inspect it. However, our subthread will never finish because it's too slow, triggering the timeout. I would argue that the correct behaviour for the system is that the However, without this PR, look at what happens if I run this code: What happened there? Well, the main thread threw a If I switch to use my version of Shelly, as per this commit, we get: My claim is that this abides to the principle of least surprise -- if sometimes takes more than it should, I would be expecting the I hope this helps! 😁 |
|
Thanks! Is there a way to add this reproducer to the testsuite? You could release this as 1.13. |
I guess we could add this pretty much unaltered in a unit test, given that it doesn't have any extra dependencies and we can assert that we should always get |
|
@andreasabel Just to be on the same page, are you waiting for me on doing anything specific in order to progress this further? I have stumbling into this problem more and more in projects at work, and it would be great to have this at least merged here, to not have to depend on a branch and not on the "official" code 😉 |
|
@adinapoli : Yes, basically, I do not want to take responsibility for this change (there is no test). I do not oppose the change, but I am not interested to develop this package further, so I would be grateful if you took over. @gregwebs Did you have time to think about this? It might be better to move this package away from a personal account to an organization so that onboarding of new maintainers is easier. |
@andreasabel I'm happy to take over maintainership of this library, given that we (Well-Typed) use it extensively for a client (IRIS Connect), which is willing to cover the time spent for improving it -- I will leave you and Greg to sort out the details, but I'm happy to deal with versioning, merging/reviewing new code, releasing things to Hackage etc as you see fit. |
|
We need to get hold of @gregwebs to (best-case) transfer this repo to an organisation. |
|
I definitely like using organizations. Let me know how I can help with long-term maintainership. |
|
There are two main options:
If you want to use haskell-pkg-janitors , I am owner there and can give out necessary rights (including admin rights for a repo that one cannot give when the repo is on a personal account). @gregwebs @adinapoli What would be your preference? |
|
@andreasabel what you and Greg find easier -- at some point I've found myself involved in the |
|
@adinapoli I invited you to haskell-pkg-janitors .
|
|
No, I did not set any restrictions. You can find my email address in the early commits of the repo. |
@andreasabel I have accepted! |
|
@gregwebs I invited you per email now. |
|
@gregwebs I see you are a member now. You should be able to transfer the repo to the organization now. (I don't think I can do this.) |

This PR uses
catchandcatchesfrom thesafe-exceptionslibrary in a few places, notably incatches_sh, which is run as part ofshelly'.Without this commit, if a
Shcomputation were being run as part of an async computation (say, asnapweb handler) if we had an async exception being thrown by the main handler, it would be caught bycatches_shand rethrown as a sync exception viaReThrownException, meaning that if the caller ofshellyhad an exception handler liketryeven if it came from thesafe-exceptionslibrary, this would be now be exercised because the exception got converted into a synchronous one.This is especially problematic because it hides the real culprit; a user might be misled thinking this was an exception coming from the guts of the
shellycomputation, whereas it was not.@andreasabel I know that dealing with exceptions (especially async ones) is always a bit of an headache, but does this sound sensible to you? I have verified in some upstream code that now the latter exhibit the correct behavior (i.e. the
trydoesn't catch it, and the exception is correctly propagated up the ladder).