packages feed

landlock-0.2.1.0: bin/landlocked.hs

module Main (main) where

import Control.Applicative (many, (<**>))
import Control.Exception.Base (displayException, handleJust)
import Control.Monad (forM_, unless)
import Data.List (intercalate, sortOn)
import Data.Maybe (listToMaybe)
import Data.Ord (Down (Down))
import Data.Version (versionBranch)
import Options.Applicative
  ( ParserInfo,
    ReadM,
    action,
    execParser,
    footer,
    fullDesc,
    header,
    help,
    helper,
    hidden,
    info,
    infoOption,
    long,
    metavar,
    noIntersperse,
    option,
    progDesc,
    str,
    strArgument,
  )
import qualified Paths_landlock as Paths
import System.Exit (ExitCode (ExitFailure), exitWith)
import System.IO (hPutStrLn, stderr)
import System.IO.Error (isDoesNotExistError, isPermissionError)
import System.Landlock
  ( AccessFsFlag,
    RulesetAttr (..),
    Version,
    abiVersion,
    accessFsFlagIsReadOnly,
    accessFsFlags,
    defaultOpenPathFlags,
    getVersion,
    isSupported,
    landlock,
    pathBeneath,
    withOpenPath,
  )
import System.Posix.Process (executeFile)

main :: IO ()
main = do
  hasLandlock <- isSupported
  unless hasLandlock $ do
    hPutStrLn stderr "Landlock not supported on this system"
    exitWith (ExitFailure 2)

  version <- abiVersion

  (usedVersion, allFlags) <- case lookupAccessFsFlags version of
    Nothing -> fail $ "Unable to retrieve file-system access flags for Landlock ABI " ++ show (getVersion version)
    Just r -> return r
  let roFlags = filter accessFsFlagIsReadOnly allFlags

  args <- execParser (parser version usedVersion)

  landlock (RulesetAttr allFlags) [] [] $ \addRule -> do
    forM_ (argsROPaths args) $ \path ->
      withOpenPath path defaultOpenPathFlags $ \fd ->
        addRule (pathBeneath fd roFlags) []

    forM_ (argsRWPaths args) $ \path ->
      withOpenPath path defaultOpenPathFlags $ \fd ->
        addRule (pathBeneath fd allFlags) []

  handleJust permissionDenied handlePermissionDenied $
    handleJust notFound handleNotFound $ do
      _ <- executeFile (argsCommand args) True (argsArguments args) Nothing
      fail "executeFile returned"
  where
    notFound e = if isDoesNotExistError e then Just e else Nothing
    handleNotFound e = do
      hPutStrLn stderr $
        unlines
          [ "Failed to execute command: " ++ displayException e
          ]
      exitWith (ExitFailure 127)
    permissionDenied e = if isPermissionError e then Just e else Nothing
    handlePermissionDenied e = do
      hPutStrLn stderr $
        unlines
          [ "Failed to execute command: " ++ displayException e,
            "Hint: access to the binary, the interpreter or shared libraries may be denied."
          ]
      exitWith (ExitFailure 126)

data Args = Args
  { argsROPaths :: [FilePath],
    argsRWPaths :: [FilePath],
    argsCommand :: FilePath,
    argsArguments :: [String]
  }
  deriving (Show, Eq)

parser :: Version -> Version -> ParserInfo Args
parser version usedVersion =
  info
    (argsParser <**> versionFlag <**> helper)
    ( fullDesc
        <> noIntersperse
        <> progDesc "Execute a command in a sandboxed environment"
        <> header
          ( unwords
              [ "Use landlocked to run a program in a sandboxed environment,",
                "restricting access to system resources using the Linux",
                "Landlock API."
              ]
          )
        <> footer
          ( unwords
              [ "The command binary, its interpreter and any shared",
                "libraries must be accessible in the sandbox."
              ]
          )
    )
  where
    argsParser =
      Args
        <$> many
          ( option
              filePath
              ( long "ro"
                  <> metavar "PATH"
                  <> help "Allow read-only access to given file or directory"
                  <> action "default"
              )
          )
        <*> many
          ( option
              filePath
              ( long "rw"
                  <> metavar "PATH"
                  <> help "Allow write access to given file or directory"
                  <> action "default"
              )
          )
        <*> strArgument
          ( metavar "COMMAND"
              <> help "Command to spawn"
              <> action "command"
          )
        <*> many
          ( strArgument
              ( metavar "ARG"
                  <> help "Arguments to pass to spawned command"
              )
          )
    versionFlag =
      infoOption
        versionString
        ( long "version"
            <> help "Show version information"
            <> hidden
        )
    versionString =
      unlines
        [ "landlocked " ++ intercalate "." (map show (versionBranch Paths.version)),
          "System Landlock ABI version: " ++ show (getVersion version),
          "Using Landlock ABI version: " ++ show (getVersion usedVersion)
        ]

-- This is not really useful. It could be if 'ReaderM' were 'MonadIO'
filePath :: ReadM FilePath
filePath = str

lookupAccessFsFlags :: Version -> Maybe (Version, [AccessFsFlag])
lookupAccessFsFlags v =
  listToMaybe $
    sortOn (Down . fst) $
      filter (\(v', _) -> v' <= v) accessFsFlags