diff --git a/Makefile b/Makefile index 88f3fd0..7902161 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,8 @@ EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/execve-linux-null-envp EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/atomic-write EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/write-EBADF EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/change-write-result +EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/flock +EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/inherited-flocks EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/mmap-syscall EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/access-itself EXAMPLE_PROGRAMS += $(EXAMPLE_DST)/sockets @@ -74,3 +76,7 @@ $(EXAMPLE_DST)/%: $(EXAMPLE_SRC)/%.c example-programs-build/mmap-syscall: example-programs/mmap-syscall.c mkdir -p example-programs-build gcc -static -std=c99 -Wall -Werror example-programs/mmap-syscall.c -o example-programs-build/mmap-syscall + +example-programs-build/inherited-flocks: example-programs/inherited-flocks.c + mkdir -p example-programs-build + gcc -static -std=c99 -Wall -Werror -pthread example-programs/inherited-flocks.c -o example-programs-build/inherited-flocks diff --git a/README.md b/README.md index 2b3f45a..76cdbfb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Includes: ## Use cases * **General** - * Get all syscalls in a list and process them programatically. + * Get all syscalls in a list and process them programmatically. * Audit high-assurance software systems. * Debug difficult bugs that occur only in certain rare situations. * Change the results of system calls as seen by the traced program. @@ -19,7 +19,7 @@ Includes: * Kill your build tool at the 3rd `write()` syscall to an `.o` file, checking whether it will recover from that in the next run. * **Testing** * Write test suites that assert how your code uses system calls, for correctness or performance. - * Mock syscalls to test how your program would behave in situations that are difficult to create in the real world, for example [veryfing fault-tolerance in specific scenarios](https://tgrez.github.io/posts/2019-09-04-simulate-network-failures.html) + * Mock syscalls to test how your program would behave in situations that are difficult to create in the real world, for example [verifying fault-tolerance in specific scenarios](https://tgrez.github.io/posts/2019-09-04-simulate-network-failures.html) * Implement anomaly test suites [like `sqlite` does](https://www.sqlite.org/testing.html#i_o_error_testing), exhaustively testing whether your program can recover from a crash in _any_ syscall. * **Fuzzing** * Insert garbage data into the program by changing syscall results or directly changing its memory contents. @@ -28,6 +28,17 @@ Includes: * Add "magic" support for new file systems without modifying existing programs (like [this paper](https://www.usenix.org/legacy/events/expcs07/papers/22-spillane.pdf) shows). * Add logging capabilities to programs that were designed without. +## Building / Installing + +* Clone: `git clone --recursive https://github.com/nh2/hatrace && cd hatrace` +* After fetching new changes, update submodules with: `git submodule init && git submodule update --init --recursive` +* Building for development: + * With [Stack](https://docs.haskellstack.org/en/stable/README/): `stack build` or even better `stack test` + * With [Cabal](https://www.haskell.org/cabal/): `cabal build` or even better `cabal test` +* Installing: + * With Stack: `stack install` + * With Cabal: `cabal install` + ## Work in progress This software is work in progress. @@ -37,12 +48,12 @@ The `hatrace` executable is extremely basic and can't do much. While syscall names are automatically generated, detail data needs to be implemented by hand and is done for only a few so far. Help to add more is appreciated. -However, the Haskell API to write scripts can already do a log. Take a look at the test suite for examples. +However, the Haskell API to write scripts can already do a lot. Take a look at the test suite for examples. ### TODO list for contributors If you find any of the below topics interesting give it a shot! -It is recommended to file an issue when picking up one of the tasks to coordinate against doing duplicate work. +It is recommended to file an issue when picking up one of the tasks to coordinate and avoid doing duplicate work. * [ ] Implement all the syscalls * [X] Remembering syscall arguments in a PID/TID map diff --git a/cabal.project b/cabal.project index 703e3a3..0eb4130 100644 --- a/cabal.project +++ b/cabal.project @@ -1,6 +1,6 @@ packages: . -# Keep deps in sync with `stack.yaml`. +-- Keep deps in sync with `stack.yaml`. source-repository-package type: git diff --git a/example-programs/flock.c b/example-programs/flock.c new file mode 100644 index 0000000..e470646 --- /dev/null +++ b/example-programs/flock.c @@ -0,0 +1,26 @@ +#include +#include +#include + +int main(int argc, char* argv[]) { + if (argc != 2) { + puts("flock /path/to/file/to/lock\n"); + return 1; + } + + const char* filepath = argv[1]; + + int fd = open(filepath, O_RDWR); + + if (fd < 0) { + perror("open"); + return 1; + } + + if (flock(fd, LOCK_EX | LOCK_NB) != 0) { + perror("flock"); + return 2; + } + + return 0; +} diff --git a/example-programs/inherited-flocks.c b/example-programs/inherited-flocks.c new file mode 100644 index 0000000..2280f48 --- /dev/null +++ b/example-programs/inherited-flocks.c @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include + +void *myThreadFun(void *vargp) { + // Store the value argument passed to this thread + int fd = *((int *)vargp); + + printf("Thread started, got passed fd %d\n", fd); + + sleep(2); + + puts("Thread exiting\n"); + + return 0; +} + +int main(int argc, char* argv[]) { + if (argc != 2) { + puts("flock /path/to/file/to/lock\n"); + return 1; + } + + const char* filepath = argv[1]; + + int fd = open(filepath, O_RDWR); + + if (fd < 0) { + perror("open"); + return 1; + } + + if (flock(fd, LOCK_EX | LOCK_NB) != 0) { + perror("flock"); + return 2; + } + + pthread_t tid; + int ret = pthread_create(&tid, NULL, myThreadFun, (void *)&fd); + + if (ret != 0) { + errno = ret; // pthread_create() does not set errno, but returns it + perror("pthread_create"); + return 3; + } + + printf("Main started side thread; closing FD %d to unlock, and sleeping for 1 second\n", fd); + + close(fd); + + sleep(1); + + puts("Main re-opening and re-locking file"); + + fd = open(filepath, O_RDWR); + + if (fd < 0) { + perror("open"); + return 1; + } + + if (flock(fd, LOCK_EX | LOCK_NB) != 0) { + perror("flock"); + return 2; + } + + pthread_exit(NULL); // exit main thread and wait for all threads to finish + + return 0; +} diff --git a/hatrace.cabal b/hatrace.cabal index ea5c79c..53f119c 100644 --- a/hatrace.cabal +++ b/hatrace.cabal @@ -34,6 +34,7 @@ library System.Hatrace.SyscallTables System.Hatrace.SyscallTables.Generated System.Hatrace.SyscallTables.Util + System.Hatrace.Tools System.Hatrace.Types System.Hatrace.Types.Internal System.Hatrace.Types.TH diff --git a/src/System/Hatrace.hs b/src/System/Hatrace.hs index 749184d..4705757 100644 --- a/src/System/Hatrace.hs +++ b/src/System/Hatrace.hs @@ -38,6 +38,8 @@ module System.Hatrace , SyscallExitDetails_access(..) , SyscallEnterDetails_faccessat(..) , SyscallExitDetails_faccessat(..) + , SyscallEnterDetails_flock(..) + , SyscallExitDetails_flock(..) , SyscallEnterDetails_write(..) , SyscallExitDetails_write(..) , SyscallEnterDetails_read(..) @@ -140,9 +142,14 @@ module System.Hatrace , SyscallExitDetails_rmdir(..) , DetailedSyscallEnter(..) , DetailedSyscallExit(..) + , EnterDetails(..) + , enterDetailsToSyscall + , ExitDetails(..) + , getExitedSyscallResult , ERRNO(..) , foreignErrnoToERRNO , getSyscallEnterDetails + , getSyscallExitDetails , setExitedSyscallResult , syscallEnterDetailsOnlyConduit , syscallRawEnterDetailsOnlyConduit @@ -161,8 +168,15 @@ module System.Hatrace , doesProcessHaveChildren , getFdPath , getExePath + -- * Simplified API that looks arguments only at syscall enter time + , HatraceEvent(..) + , EventDetails(..) + , EventSyscallEnterDetails(..) + , EventSyscallExitDetails(..) + , ReturnOrErrno(..) -- * Re-exports , KnownSyscall(..) + , CPid ) where import Conduit (concatMapC, foldlC) @@ -846,6 +860,29 @@ instance SyscallExitFormatting SyscallExitDetails_faccessat where (syscallEnterToFormatted enterDetail, NoReturn) +data SyscallEnterDetails_flock = SyscallEnterDetails_flock + { fd :: CInt + , operation :: CInt + -- Peeked details + , flockOperation :: FlockOperation + } deriving (Eq, Ord, Show) + +instance SyscallEnterFormatting SyscallEnterDetails_flock where + syscallEnterToFormatted SyscallEnterDetails_flock{ fd, operation, flockOperation } = + FormattedSyscall "flock" [ formatArg fd, formatArg operation + , formatArg flockOperation + ] + + +data SyscallExitDetails_flock = SyscallExitDetails_flock + { enterDetail :: SyscallEnterDetails_flock + } deriving (Eq, Ord, Show) + +instance SyscallExitFormatting SyscallExitDetails_flock where + syscallExitToFormatted SyscallExitDetails_flock{ enterDetail } = + (syscallEnterToFormatted enterDetail, NoReturn) + + data SyscallEnterDetails_stat = SyscallEnterDetails_stat { pathname :: Ptr CChar , statbuf :: Ptr StatStruct @@ -1844,6 +1881,7 @@ data DetailedSyscallEnter | DetailedSyscallEnter_pipe2 SyscallEnterDetails_pipe2 | DetailedSyscallEnter_access SyscallEnterDetails_access | DetailedSyscallEnter_faccessat SyscallEnterDetails_faccessat + | DetailedSyscallEnter_flock SyscallEnterDetails_flock | DetailedSyscallEnter_write SyscallEnterDetails_write | DetailedSyscallEnter_read SyscallEnterDetails_read | DetailedSyscallEnter_execve SyscallEnterDetails_execve @@ -1907,6 +1945,7 @@ data DetailedSyscallExit | DetailedSyscallExit_pipe2 SyscallExitDetails_pipe2 | DetailedSyscallExit_access SyscallExitDetails_access | DetailedSyscallExit_faccessat SyscallExitDetails_faccessat + | DetailedSyscallExit_flock SyscallExitDetails_flock | DetailedSyscallExit_write SyscallExitDetails_write | DetailedSyscallExit_read SyscallExitDetails_read | DetailedSyscallExit_execve SyscallExitDetails_execve @@ -2057,6 +2096,13 @@ getSyscallEnterDetails syscall syscallArgs pid = let proc = TracedProcess pid in , pathnameBS , flags = fromIntegral flags } + Syscall_flock -> do + let SyscallArgs{ arg0 = fd, arg1 = operation } = syscallArgs + pure $ DetailedSyscallEnter_flock $ SyscallEnterDetails_flock + { fd = fromIntegral fd + , operation = fromIntegral operation + , flockOperation = fromCInt (fromIntegral operation) + } Syscall_write -> do let SyscallArgs{ arg0 = fd, arg1 = bufAddr, arg2 = count } = syscallArgs let bufPtr = word64ToPtr bufAddr @@ -2619,6 +2665,11 @@ getSyscallExitDetails detailedSyscallEnter result pid = pure $ DetailedSyscallExit_faccessat $ SyscallExitDetails_faccessat{ enterDetail } + DetailedSyscallEnter_flock + enterDetail@SyscallEnterDetails_flock{} -> do + pure $ DetailedSyscallExit_flock $ + SyscallExitDetails_flock{ enterDetail } + DetailedSyscallEnter_read enterDetail@SyscallEnterDetails_read{ buf } -> do bufContents <- peekBytes (TracedProcess pid) buf (fromIntegral result) @@ -3312,6 +3363,8 @@ formatSyscallEnter enterDetails = DetailedSyscallEnter_faccessat details -> syscallEnterToFormatted details + DetailedSyscallEnter_flock details -> syscallEnterToFormatted details + DetailedSyscallEnter_write details -> syscallEnterToFormatted details DetailedSyscallEnter_read details -> syscallEnterToFormatted details @@ -3467,6 +3520,8 @@ formatDetailedSyscallExit detailedExit handleUnimplemented = DetailedSyscallExit_faccessat details -> formatDetails details + DetailedSyscallExit_flock details -> formatDetails details + DetailedSyscallExit_write details -> formatDetails details DetailedSyscallExit_read details -> formatDetails details diff --git a/src/System/Hatrace/Main.hs b/src/System/Hatrace/Main.hs index 647fb31..94cf94d 100644 --- a/src/System/Hatrace/Main.hs +++ b/src/System/Hatrace/Main.hs @@ -19,9 +19,11 @@ import System.FilePath (splitPath) import System.Hatrace import System.Hatrace.Format +import System.Hatrace.Tools -data Filter = - FilterAtomicWrites +data Filter + = FilterAtomicWrites + | FilterIntheritedFlocks deriving (Eq, Ord, Show) -- | Command line arguments of this program. @@ -50,11 +52,15 @@ cliArgsParser = do modeParser :: Parser RunMode modeParser = - filterParser Opts.<|> traceParser + filterParser Opts.<|> filterInheritedFlocksParser Opts.<|> traceParser where filterParser = flag' (FilterMode FilterAtomicWrites) ( long "find-nonatomic-writes" <> help "find file writes without a following rename to a persistent location" ) + filterInheritedFlocksParser = + flag' (FilterMode FilterIntheritedFlocks) + ( long "find-inherited-flocks" + <> help "find flock()s on locks that were inherided forked threads/processes" ) traceParser = TraceMode <$> (traceJsonParser Opts.<|> traceStdParser) traceJsonParser = flag' JsonOutput ( long "json-output" @@ -122,6 +128,10 @@ main = do forM_ bad $ \(p, e) -> do putStrLn $ " - " ++ show p ++ ": " ++ e exitWith exitCode + FilterMode FilterIntheritedFlocks -> do + argv <- procToArgv cliProgram cliArgs + res <- sourceTraceForkExecvFullPathWithSink argv inheritedFlocksSink + print res maybeNonatomicOrBad :: FilePath -> FileWriteBehavior -> Maybe (Either FilePath (FilePath, String)) maybeNonatomicOrBad _ NoWrites = Nothing diff --git a/src/System/Hatrace/Tools.hs b/src/System/Hatrace/Tools.hs new file mode 100644 index 0000000..9fe6476 --- /dev/null +++ b/src/System/Hatrace/Tools.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} + +module System.Hatrace.Tools + ( inheritedFlocksSink + ) where + +import Control.Monad.IO.Class (MonadIO, liftIO) +import Data.Conduit + +import System.Hatrace + +inheritedFlocksSink :: + (MonadIO m) + => ConduitT (CPid, TraceEvent EnterDetails) Void m () +inheritedFlocksSink = do + -- extract <$> (fileWritesConduit .| foldlC collectWrite Map.empty) + let loop = do + await >>= \case + -- Just (pid :: CPid, ev :: Either (Syscall, ERRNO) DetailedSyscallExit) -> do + Nothing -> return () + Just (HatraceEvent pid (eventDetails :: EventDetails)) -> do + case eventDetails of + EventSyscallEnter EventSyscallEnterDetails{ evEnterDetails } -> case evEnterDetails of + KnownEnterDetails knownSyscall (DetailedSyscallEnter_flock SyscallEnterDetails_flock{ fd, flockOperation }) -> do + liftIO $ print ("enter flock", fd, flockOperation) + -- Just errno -> case syscall of + -- KnownSyscall Syscall_flock -> liftIO $ print ("flock returned error", errno) + -- _ -> return () + -- Nothing -> case enterDetails of + -- KnownEnterDetails _knownSyscall detailedSyscallEnter -> do + -- details <- liftIO $ getSyscallExitDetails detailedSyscallEnter result pid + -- liftIO $ print details + _ -> return () + + EventSyscallExit EventSyscallExitDetails{ evExitDetails } -> case evExitDetails of + KnownExitDetails knownSyscall _detailedSyscallEnter -> do + liftIO $ print ("exit", knownSyscall) + _ -> return () + + _ -> return () + loop + formatHatraceEventConduit .| loop diff --git a/src/System/Hatrace/Types.hsc b/src/System/Hatrace/Types.hsc index 4bd1908..3f01e1f 100644 --- a/src/System/Hatrace/Types.hsc +++ b/src/System/Hatrace/Types.hsc @@ -9,6 +9,7 @@ #endif #include +#include #include #include #include @@ -21,6 +22,7 @@ module System.Hatrace.Types ( FileAccessMode(..) + , FlockOperation(..) , GranularAccessMode(..) , MemoryProtectMode(..) , GranularMemoryProtectMode(..) @@ -122,6 +124,38 @@ $(deriveFlagsCIntRepresentable ''FileAccessMode , ('accessModeExecute, (#const X_OK)) ]) +data FlockOperation + = FlockShared Bool -- ^ nonblocking? + | FlockExclusive Bool -- ^ nonblocking? + | FlockUnlock + | FlockUnknown CInt + deriving (Eq, Ord, Show) + +instance CIntRepresentable FlockOperation where + toCInt (FlockShared False) = (#const LOCK_SH) + toCInt (FlockShared True) = (#const LOCK_SH) .|. (#const LOCK_NB) + toCInt (FlockExclusive False) = (#const LOCK_EX) + toCInt (FlockExclusive True) = (#const LOCK_EX) .|. (#const LOCK_NB) + toCInt FlockUnlock = (#const LOCK_UN) + toCInt (FlockUnknown x) = x + + fromCInt x + | x == (#const LOCK_SH) = FlockShared False + | x == (#const LOCK_SH) .|. (#const LOCK_NB) = FlockShared True + | x == (#const LOCK_EX) = FlockExclusive False + | x == (#const LOCK_EX) .|. (#const LOCK_NB) = FlockExclusive True + | x == (#const LOCK_UN) = FlockUnlock + | otherwise = FlockUnknown x + +instance ArgFormatting FlockOperation where + formatArg (FlockShared False) = FixedStringArg "LOCK_SH" + formatArg (FlockShared True) = FixedStringArg "LOCK_SH | LOCK_NB" + formatArg (FlockExclusive False) = FixedStringArg "LOCK_EX" + formatArg (FlockExclusive True) = FixedStringArg "LOCK_EX | LOCK_NB" + formatArg FlockUnlock = FixedStringArg "LOCK_UN" + formatArg (FlockUnknown unknown) = + IntegerArg (fromIntegral unknown) + data MemoryProtectMode = MemoryProtectKnown GranularMemoryProtectMode | MemoryProtectUnknown CInt