Explicit filesystem permissions required

This commit is contained in:
2026-05-12 19:02:51 -05:00
parent d6df01105c
commit 983a0cc5a7
5 changed files with 150 additions and 43 deletions

View File

@@ -36,21 +36,22 @@ tricu < -- or calculate its size (/demos/size.tri)
tricu < size not? tricu < size not?
tricu > 12 tricu > 12
tricu < !help tricu < !help
tricu version 1.1.0 tricu version 1.1.0
Available commands: Available commands:
!exit - Exit the REPL !exit - Exit the REPL
!clear - Clear the screen !clear - Clear the screen
!reset - Reset preferences for selected versions !reset - Reset preferences for selected versions
!help - Show tricu version and available commands !help - Show tricu version and available commands
!output - Change output format (tree|fsl|ast|ternary|ascii|decode) !output - Change output format (tree|fsl|ast|ternary|ascii|decode)
!definitions - List all defined terms in the content store !definitions - List all defined terms in the content store
!import - Import definitions from file to the content store !import - Import definitions from file to the content store
!watch - Watch a file for changes, evaluate terms, and store them !watch - Watch a file for changes, evaluate terms, and store them
!refresh - Refresh environment from content store (definitions are live) !versions - Show all versions of a term by name
!versions - Show all versions of a term by name !select - Select a specific version of a term for subsequent lookups
!select - Select a specific version of a term for subsequent lookups !tag - Add or update a tag for a term by hash or name
!tag - Add or update a tag for a term by hash or name !export - Export a term bundle to file (hash, file)
!bundleimport- Import a bundle file into the content store
``` ```
## Installation and Use ## Installation and Use

View File

@@ -1,6 +1,7 @@
module IODriver module IODriver
( IOPermissions(..) ( IOPermissions(..)
, defaultPerms , defaultPerms
, unsafePerms
, checkIOSentinel , checkIOSentinel
, runIO , runIO
) where ) where
@@ -8,19 +9,25 @@ module IODriver
import Research (T(..), apply, toString, toNumber, ofString, ofNumber) import Research (T(..), apply, toString, toNumber, ofString, ofNumber)
import System.IO (putStr, getLine) import System.IO (putStr, getLine)
import qualified System.IO as IO import qualified System.IO as IO
import Control.Exception (try, IOException) import Control.Exception (try, IOException, SomeException)
import Control.Monad (unless)
import System.Exit (die) import System.Exit (die)
import System.IO.Error (isDoesNotExistError, isPermissionError, isAlreadyExistsError) import System.IO.Error (isDoesNotExistError, isPermissionError, isAlreadyExistsError)
import Data.List (isPrefixOf) import Data.List (isPrefixOf)
import System.FilePath (normalise, isRelative, (</>), addTrailingPathSeparator, splitDirectories)
import System.Directory (canonicalizePath, doesPathExist, getCurrentDirectory)
data IOPermissions = IOPermissions data IOPermissions = IOPermissions
{ allowRead :: [FilePath] { allowRead :: [FilePath]
, allowWrite :: [FilePath] , allowWrite :: [FilePath]
, allowReadAll :: Bool
, allowWriteAll :: Bool
} }
defaultPerms :: IOPermissions defaultPerms :: IOPermissions
defaultPerms = IOPermissions [] [] defaultPerms = IOPermissions [] [] False False
unsafePerms :: IOPermissions
unsafePerms = IOPermissions [] [] True True
checkIOSentinel :: T -> Either String (Integer, T) checkIOSentinel :: T -> Either String (Integer, T)
checkIOSentinel tree = checkIOSentinel tree =
@@ -63,9 +70,12 @@ runIO perms actionTree = go actionTree
3 -> case payload of 3 -> case payload of
Fork path k -> do Fork path k -> do
p <- decodeString path "ReadFile" p <- decodeString path "ReadFile"
checkReadPerm p mDeny <- checkReadPerm p
content <- tryReadFile p case mDeny of
go (apply k content) Just denied -> go (apply k denied)
Nothing -> do
content <- tryReadFile p
go (apply k content)
_ -> die "Invalid ReadFile payload: expected pair path continuation" _ -> die "Invalid ReadFile payload: expected pair path continuation"
4 -> case payload of 4 -> case payload of
@@ -73,9 +83,12 @@ runIO perms actionTree = go actionTree
Fork contents k -> do Fork contents k -> do
p <- decodeString path "WriteFile" p <- decodeString path "WriteFile"
c <- decodeString contents "WriteFile" c <- decodeString contents "WriteFile"
checkWritePerm p mDeny <- checkWritePerm p
res <- tryWriteFile p c case mDeny of
go (apply k res) Just denied -> go (apply k denied)
Nothing -> do
res <- tryWriteFile p c
go (apply k res)
_ -> die "Invalid WriteFile payload: expected pair contents continuation" _ -> die "Invalid WriteFile payload: expected pair contents continuation"
_ -> die "Invalid WriteFile payload: expected pair path (pair contents continuation)" _ -> die "Invalid WriteFile payload: expected pair path (pair contents continuation)"
@@ -87,15 +100,80 @@ runIO perms actionTree = go actionTree
Left err -> die $ "Invalid " ++ ctx ++ " string: " ++ err Left err -> die $ "Invalid " ++ ctx ++ " string: " ++ err
checkReadPerm p = checkReadPerm p =
unless (pathAllowed p (allowRead perms)) $ if allowReadAll perms
die $ "Permission denied: read not allowed for " ++ p then return Nothing
else do
mp <- canonicalizeSafe p
case mp of
Left _ -> return $ Just policyErrResult
Right path -> do
allowed <- pathAllowed path (allowRead perms)
if allowed
then return Nothing
else return $ Just policyErrResult
checkWritePerm p = checkWritePerm p =
unless (pathAllowed p (allowWrite perms)) $ if allowWriteAll perms
die $ "Permission denied: write not allowed for " ++ p then return Nothing
else do
mp <- canonicalizeSafe p
case mp of
Left _ -> return $ Just policyErrResult
Right path -> do
allowed <- pathAllowed path (allowWrite perms)
if allowed
then return Nothing
else return $ Just policyErrResult
pathAllowed _ [] = True -- No restrictions policyErrResult = errResult 5
pathAllowed p prefixes = any (\prefix -> prefix `isPrefixOf` p) prefixes
canonicalizeSafe :: FilePath -> IO (Either String FilePath)
canonicalizeSafe p = do
exists <- doesPathExist p
if exists
then do
result <- try (canonicalizePath p) :: IO (Either SomeException FilePath)
case result of
Right canon -> return $ Right canon
Left _ -> normalizeSyntactic p
else normalizeSyntactic p
normalizeSyntactic :: FilePath -> IO (Either String FilePath)
normalizeSyntactic p = do
absPath <- if isRelative p then (</> p) <$> getCurrentDirectory else return p
let norm = normalise absPath
dirs = splitDirectories norm
if ".." `elem` dirs
then return $ Left "Path contains unresolved parent-directory references"
else return $ Right norm
pathAllowed :: FilePath -> [FilePath] -> IO Bool
pathAllowed _ [] = return False
pathAllowed p prefixes = do
let validPrefixes = filter (not . null) prefixes
if null validPrefixes
then return False
else do
absPrefixes <- mapM resolvePrefix validPrefixes
return $ any (isPathPrefixOf p) absPrefixes
resolvePrefix :: FilePath -> IO FilePath
resolvePrefix p = do
let norm = normalise p
absPath <- if isRelative norm then (</> norm) <$> getCurrentDirectory else return norm
exists <- doesPathExist absPath
if exists
then do
result <- try (canonicalizePath absPath) :: IO (Either SomeException FilePath)
case result of
Right canon -> return canon
Left _ -> return absPath
else return absPath
isPathPrefixOf :: FilePath -> FilePath -> Bool
isPathPrefixOf path prefix =
let prefix' = addTrailingPathSeparator prefix
in path == prefix || prefix' `isPrefixOf` path
tryReadFile path = do tryReadFile path = do
result <- try (IO.readFile path) :: IO (Either IOException String) result <- try (IO.readFile path) :: IO (Either IOException String)

View File

@@ -33,13 +33,16 @@ import System.Environment (lookupEnv)
data TricuArgs data TricuArgs
= Repl = Repl
| Eval | Eval
{ evalFiles :: [FilePath] { evalFiles :: [FilePath]
, evalFormat :: EvaluatedForm , evalFormat :: EvaluatedForm
, evalOutput :: FilePath , evalOutput :: FilePath
, evalDb :: Maybe FilePath , evalDb :: Maybe FilePath
, evalIo :: Bool , evalIo :: Bool
, evalAllowRead :: [FilePath] , evalAllowRead :: [FilePath]
, evalAllowWrite :: [FilePath] , evalAllowWrite :: [FilePath]
, evalAllowReadAll :: Bool
, evalAllowWriteAll :: Bool
, evalUnsafeIo :: Bool
} }
| ArboricxCompile | ArboricxCompile
{ compileInput :: FilePath { compileInput :: FilePath
@@ -116,6 +119,18 @@ evalParser = Eval
<> metavar "PATH" <> metavar "PATH"
<> help "Allow writing to PATH prefix (repeatable)" <> help "Allow writing to PATH prefix (repeatable)"
)) ))
<*> switch
( long "allow-read-all"
<> help "Allow reading from any path"
)
<*> switch
( long "allow-write-all"
<> help "Allow writing to any path"
)
<*> switch
( long "unsafe-io"
<> help "Allow unrestricted read and write access"
)
compileParser :: Parser TricuArgs compileParser :: Parser TricuArgs
compileParser = ArboricxCompile compileParser = ArboricxCompile
@@ -294,7 +309,12 @@ runEval opts = do
finalT <- if evalIo opts finalT <- if evalIo opts
then case checkIOSentinel resultT of then case checkIOSentinel resultT of
Right (1, action) -> do Right (1, action) -> do
let perms = IOPermissions (evalAllowRead opts) (evalAllowWrite opts) let perms = IOPermissions
{ allowRead = evalAllowRead opts
, allowWrite = evalAllowWrite opts
, allowReadAll = evalUnsafeIo opts || evalAllowReadAll opts
, allowWriteAll = evalUnsafeIo opts || evalAllowWriteAll opts
}
runIO perms action runIO perms action
Right (v, _) -> die $ "Unsupported IO ABI version: " ++ show v Right (v, _) -> die $ "Unsupported IO ABI version: " ++ show v
Left err -> die $ "IO mode requested but " ++ err Left err -> die $ "IO mode requested but " ++ err

View File

@@ -227,10 +227,18 @@ toNumber (Fork (Stem Leaf) rest) = case toNumber rest of
Left err -> Left err Left err -> Left err
toNumber _ = Left "Invalid Tree Calculus number" toNumber _ = Left "Invalid Tree Calculus number"
toChar :: Integer -> Either String Char
toChar n
| n < 0 = Left "Negative character code"
| n > 0x10FFFF = Left "Character code out of Unicode range"
| n >= 0xD800 && n <= 0xDFFF = Left "Surrogate character code not allowed"
| otherwise = Right (toEnum (fromInteger n))
toString :: T -> Either String String toString :: T -> Either String String
toString tc = case toList tc of toString tc = do
Right list -> traverse (fmap (toEnum . fromInteger) . toNumber) list list <- toList tc
Left _ -> Left "Invalid Tree Calculus string" nums <- mapM toNumber list
mapM toChar nums
toList :: T -> Either String [T] toList :: T -> Either String [T]
toList Leaf = Right [] toList Leaf = Right []

View File

@@ -8,7 +8,7 @@ import REPL
import Research import Research
import Wire import Wire
import ContentStore import ContentStore
import IODriver import IODriver (IOPermissions(..), checkIOSentinel, runIO, unsafePerms)
import Control.Exception (evaluate, try, SomeException) import Control.Exception (evaluate, try, SomeException)
import Control.Monad (forM_) import Control.Monad (forM_)
@@ -1307,6 +1307,6 @@ runIOSource source = do
ioEnv <- evaluateFile "./lib/io.tri" ioEnv <- evaluateFile "./lib/io.tri"
env <- evalTricuWithStore Nothing ioEnv (parseTricu source) env <- evalTricuWithStore Nothing ioEnv (parseTricu source)
case checkIOSentinel (mainResult env) of case checkIOSentinel (mainResult env) of
Right (1, action) -> runIO defaultPerms action Right (1, action) -> runIO unsafePerms action
Right (v, _) -> assertFailure ("Unsupported IO ABI version: " ++ show v) Right (v, _) -> assertFailure ("Unsupported IO ABI version: " ++ show v)
Left err -> assertFailure ("Expected IO sentinel: " ++ err) Left err -> assertFailure ("Expected IO sentinel: " ++ err)