mueval 0.4.5 → 0.4.6
raw patch · 7 files changed
+111/−35 lines, 7 files
Files
- Mueval/Concurrent.hs +18/−15
- Mueval/Context.hs +2/−1
- Mueval/Interpreter.hs +20/−5
- Mueval/ParseArgs.hs +5/−4
- Mueval/Resources.hs +3/−3
- README +53/−0
- mueval.cabal +10/−7
Mueval/Concurrent.hs view
@@ -3,13 +3,13 @@ import Control.Concurrent (forkIO, killThread, myThreadId, threadDelay, throwTo, yield, ThreadId) import System.Posix.Signals (sigXCPU, installHandler, Handler(CatchOnce)) import Control.Exception (catchDyn, Exception(ErrorCall))-import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)+import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar, MVar) import System.IO (hSetBuffering, stdout, BufferMode(NoBuffering)) import Mueval.Interpreter import Mueval.ParseArgs --- | Fork off a thread which will sleep and kill off another thread at some point.+-- | Fork off a thread which will sleep and then kill off the specified thread. watchDog :: Int -> ThreadId -> IO () watchDog tout tid = do installHandler sigXCPU (CatchOnce@@ -23,22 +23,25 @@ return () -- Never reached. Either we error out in -- watchDog, or the evaluation thread finishes. --- | Set a watchdog on this thread, and then evaluate.-forkedMain :: Options -> IO ()-forkedMain opts = do- -- This *should* be redundant with the previous watchDog,- -- but maybe not.- mvar <- newEmptyMVar-- myThreadId >>= watchDog tout+-- | A basic blocking operation.+block :: (t -> MVar a -> IO t1) -> t -> IO a+block f opts = do mvar <- newEmptyMVar+ f opts mvar+ takeMVar mvar -- block until ErrorCall, or forkedMain succeeds - hSetBuffering stdout NoBuffering+-- | Using MVars, block on forkedMain' until it finishes.+forkedMain :: Options -> IO ()+forkedMain opts = block forkedMain' opts >> return () - -- Our modules and expression are set up. Let's do stuff.- forkIO (interpreterSession typeprint mdls expr `catchDyn` (printInterpreterError) >> putMVar mvar "Done.")+-- | Set a 'watchDog' on this thread, and then continue on with whatever.+forkedMain' :: Options -> MVar [Char] -> IO ThreadId+forkedMain' opts mvar = do myThreadId >>= watchDog tout+ hSetBuffering stdout NoBuffering - takeMVar mvar -- block until ErrorCall, or forkedMain succeeds- return ()+ -- Our modules and expression are set up. Let's do stuff.+ forkIO (interpreterSession typeprint mdls expr+ `catchDyn` (printInterpreterError)+ >> putMVar mvar "Done.") where mdls = modules opts expr = expression opts tout = timeLimit opts
Mueval/Context.hs view
@@ -4,7 +4,8 @@ {- | Return true if the String contains anywhere in it any keywords associated with dangerous functions. Unfortunately, this blacklist leaks like a sieve- and will return many false positives (eg. unsafed "id \"unsafed\"" -> True). But it+ and will return many false positives (eg. 'unsafed "id \"unsafed\""' will+ evaluate to True, even though the phrase \"unsafe\" appears only in a String). But it will at least catch naive and simplistic invocations of "unsafePerformIO", "inlinePerformIO", and "unsafeCoerce". -} unsafe :: String -> Bool
Mueval/Interpreter.hs view
@@ -1,5 +1,5 @@ -- TODO: suggest the convenience functions be put into Hint proper?-module Mueval.Interpreter (interpreterSession, printInterpreterError, ModuleName) where+module Mueval.Interpreter where import Control.Monad.Trans (liftIO) import qualified Control.Exception (catch)@@ -11,17 +11,26 @@ import qualified Mueval.Resources (limitResources) -+-- | From inside the Interpreter monad, print the String (presumably the result+-- of interpreting something), but only print the first 1024 characters to avoid+-- flooding. Lambdabot has a similar limit. say :: String -> Interpreter () say = liftIO . putStr . take 1024 +-- | Oh no, something has gone wrong. Call 'error' and then, as with 'say',+-- print out a maximum of 1024 characters. printInterpreterError :: InterpreterError -> IO () printInterpreterError = error . take 1024 . ("Oops... " ++) . show +{- | The actual calling of Hint functionality. The heart of this just calls+ 'eval', but we do so much more - we disable Haskell extensions, turn on+ optimizations, hide all packages, make sure one cannot call unimported+ functions, typecheck (and optionally print it), set resource limits for this+ thread, and do some error handling. -} interpreter :: Bool -> [ModuleName] -> String -> Interpreter () interpreter prt modules expr = do- setUseLanguageExtensions False -- Don't trust the- -- extensions+ setUseLanguageExtensions True -- Don't trust the+ -- extensions setOptimizations All -- Maybe optimization will make -- more programs terminate. reset -- Make sure nothing is available@@ -38,7 +47,13 @@ say $ show result ++ "\n" else error "Expression did not type check." -interpreterSession :: Bool -> [ModuleName] -> String -> IO ()+-- | Wrapper around 'interpreter'; supplies a fresh GHC API session and+-- error-handling. The arguments are simply passed on.+interpreterSession :: Bool -- ^ Whether to print inferred type+ -> [ModuleName] -- ^ A list of modules we wish to be visible+ -> String -- ^ The string to be interpreted as a Haskell expression+ -> IO () -- ^ No real result, since printing is done deeper in+ -- the stack. interpreterSession prt mds expr = Control.Exception.catch (newSession >>= (flip withSession) (interpreter prt mds expr)) (\_ -> error "Expression did not compile.")
Mueval/ParseArgs.hs view
@@ -5,6 +5,7 @@ import Mueval.Context (defaultModules) +-- | See the results of --help for information on what each option means. data Options = Options { timeLimit :: Int , modules :: [String]@@ -22,7 +23,7 @@ options :: [OptDescr (Options -> Options)] options = [Option ['p'] ["password"]- (ReqArg (\u opts -> opts {user = u}) "PASS")+ (ReqArg (\u opts -> opts {user = u}) "PASSWORD") "The password for the mubot account. If this is set, mueval will attempt to setuid to the mubot user. This is optional, as it requires the mubot user to be set up properly. (Currently a null-op.)", Option ['t'] ["timelimit"] (ReqArg (\t opts -> opts { timeLimit = (read t :: Int) }) "TIME")@@ -31,11 +32,11 @@ (ReqArg (\m opts -> opts { modules = m:(modules opts) }) "MODULE") "A module we should import functions from for evaluation. (Can be given multiple times.)", Option ['e'] ["expression"]- (ReqArg (\e opts -> opts { expression = e}) "EXPR")+ (ReqArg (\e opts -> opts { expression = e}) "EXPRESSION") "The expression to be evaluated.",- Option ['p'] ["print-type"]+ Option ['i'] ["inferred-type"] (NoArg (\opts -> opts { printType = True}))- "Whether to enable printing of inferred type." ]+ "Whether to enable printing of inferred type. Defaults to false." ] interpreterOpts :: [String] -> IO (Options, [String]) interpreterOpts argv =
Mueval/Resources.hs view
@@ -5,7 +5,7 @@ import System.Directory (setCurrentDirectory) -- | Pull together several methods of reducing priority and easy access to resources:--- nice, rlimits, and "cd".+-- 'nice', the rlimit bindings, and "setCurrentDirectory". limitResources :: IO () limitResources = do setCurrentDirectory "/tmp" -- will at least mess up relative links nice 19 -- Set our process priority way down@@ -21,14 +21,14 @@ -- These limits seem to be useless? stackSizeLimitSoft = zero stackSizeLimitHard = zero--- We allow one file to be opened, package.conf, because it is necessary. This+-- We allow a few files to be opened, such as package.conf, because they are necessary. This -- doesn't seem to be security problem because it'll be opened at the module -- stage, before code ever evaluates. I hope. openFilesLimitSoft = openFilesLimitHard openFilesLimitHard = ResourceLimit 7 -- TODO: It would be nice to set these to zero, but right now Hint gets around the -- insecurity of the GHC API by writing stuff out to a file in /tmp, so we need--- to allow our compiled binary to do file I/O... :( But at least we can limit+-- to allow our compiled binary to do file I/O... :( But at least we can still limit -- how much we write out! fileSizeLimitSoft = fileSizeLimitHard fileSizeLimitHard = ResourceLimit 590
+ README view
@@ -0,0 +1,53 @@+WHAT:+Mueval grew out of my discontent with Lambdabot: it's really neat to be able to run expressions like this:++07:53 < ivanm> > filter (\ x -> isLetter x || x == '\t') "asdf$#$ dfs"+07:55 < lambdabot> "asdfdfs"++But Lambdabot is crufty and very difficult to install or run. IMO, we need a replacement or rewrite, but one of the things that make this difficult is that Lambdabot uses hs-plugins to get that sort of evaluation functionality, and hs-plugins is half the problem. We want some sort of standalone executable which provides that functionality. Now, 'ghc -e' is obviously unsuited because there is no sandboxing, so what I've done is basically marry the GHC API (as rendered less sharp-edged by Hint) with a bunch of resource limits and sandboxing (as largely stolen from Lambdabot).++EXAMPLES:+The end result is an adorable little program, which you can use like this:++ bash-3.2$ mueval --expression '1*100+1'+ Expression type: (Num t) => t+ result: "101"++ bash-3.2$ mueval --expression "filter (\`notElem\` ['A'..'Z']) \"abcXsdzWEE\""+ Expression type: [Char]+ result: "\"abcsdz\""++Note that mueval will avoid all the attacks I've been able to test on it:++ bash-3.2$ mueval --expression 'let x = x in x'+ Expression type: t+ result: "mueval: Time limit exceeded++ bash-3.2$ mueval --expression "let foo = readFile \"/etc/passwd\" >>= print in foo"+ Expression type: IO ()+ result: "<IO ()>"++ bash-3.2$ mueval --module System.IO.Unsafe --expression "let foo = unsafePerformIO readFile \"/etc/passwd\" in foo"+ mueval: Unknown or untrusted module supplied! Aborting.++SUMMARY:+Anyway, it's my hope that this will be useful as an example or useful in itself for people endeavouring to fix the Lambdabot situation or just in safely running code period.++GETTING:+You can download Mueval at the usual place: <http://hackage.haskell.org/cgi-bin/hackage-scripts/package/mueval>. Mueval has a public darcs repository, at <http://code.haskell.org/mubot/> (in the mueval/ subdirectory). Contributions are of course welcomed.++INSTALLING:+Mueval depends on a few of the standard libraries, which you should have installed already, and also on the 'Hint' library <http://hackage.haskell.org/cgi-bin/hackage-scripts/package/hint>; Hint is particularly essential as it is the very capable wrapper around the GHC API which Mueval uses. (Without Hint, this would've been even more painful to write). All of this is cabalized, so ideally installation will be as simple as:++ sh$ cabal install mueval++However, you can still manually download and unpack the Mueval tarball, and do the usual Cabal dance:++ sh$ runhaskell Setup configure+ sh$ runhaskell Setup build+ sh$ runhaskell Setup install++BUGS:+Mueval uses a number of techniques for security; particularly problematic seem to be the resource limits, as they have to be specified manually & statically in the source code and so will probably be broken somewhere somewhen. If you can successfully build &install Mueval, but running it on expressions leads to errors, please send me an email at <gwern0@gmail.com>. Include in the email all the output you see if you run the informal test suite:++ sh$ sh tests.sh
mueval.cabal view
@@ -1,5 +1,5 @@ name: mueval-version: 0.4.5+version: 0.4.6 license: BSD3 license-file: LICENSE@@ -10,20 +10,23 @@ synopsis: Safely evaluate Haskell expressions description: Mueval is a Haskell interpreter. It uses the GHC API to evaluate arbitrary Haskell expressions.- Importantly, mueval takes many precautions to defang and avoid "evil"+ Importantly, mueval takes many precautions to defang and avoid \"evil\" code. It uses resource limits, whitelisted modules, special Show instances for IO, threads, changes of directory, and so- on to sandbox the Haskell code. (It is much like Lambdabot's famous- evaluation functionality.)+ on to sandbox the Haskell code. .+ It is, in short, intended to be a standalone version of Lambdabot's famous+ evaluation functionality. For examples and explanations, please see the README file.+ . Mueval is currently POSIX-only. homepage: http://code.haskell.org/mubot/ build-type: Simple-Cabal-Version: >= 1.2-Tested-with: GHC==6.8.2+cabal-version: >= 1.2+tested-with: GHC==6.8.2 -Extra-source-files: build.sh, tests.sh+data-files: README+extra-source-files: build.sh, tests.sh Library exposed-modules: Mueval.Concurrent, Mueval.Context, Mueval.Interpreter,