ppad-censor-0.5.1: lib/Censor/Runner/DL.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module: Censor.Runner.DL
-- Copyright: (c) 2026 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Minimal @dlopen@\/@dlsym@ bindings for driving a foreign
-- constant-time target that lives in a shared library. Used by the
-- @censor@ executable to resolve a symbol at runtime and hand it to
-- "Censor.FFI" as an @ffiTarget@.
--
-- The underlying handle is opened with @RTLD_NOW | RTLD_LOCAL@: any
-- unresolved symbols surface immediately (so bad shims are caught at
-- 'withLibrary' entry, not at first call), and the library's own
-- symbols do not pollute the global namespace. Not available on
-- Windows.
module Censor.Runner.DL (
-- * Library handle
Library
, withLibrary
-- * Resolving targets
, resolveTarget
-- * Errors
, DLError(..)
) where
import Control.Exception (Exception, bracket, throwIO)
import Data.Word (Word8)
import Foreign.C.String (CString, peekCString, withCString)
import Foreign.Ptr (FunPtr, Ptr, castPtrToFunPtr, nullPtr)
foreign import ccall unsafe "censor_dlopen"
c_dlopen :: CString -> IO (Ptr ())
foreign import ccall unsafe "censor_dlsym"
c_dlsym :: Ptr () -> CString -> IO (Ptr ())
foreign import ccall unsafe "censor_dlclose"
c_dlclose :: Ptr () -> IO Int
foreign import ccall unsafe "censor_dlerror"
c_dlerror :: IO CString
-- unsafe: this is the timed call, and a safe one would put the RTS's
-- suspend/resume of the calling thread inside the timed region.
foreign import ccall unsafe "dynamic"
mkTargetFun :: FunPtr (Ptr Word8 -> IO ()) -> Ptr Word8 -> IO ()
-- | An opened shared library. Bracketed by 'withLibrary'; do not
-- retain the value past its callback.
newtype Library = Library (Ptr ())
-- | Why a dynamic-linker operation failed. The 'String' payload is
-- the @dlerror@ message at the moment of failure.
data DLError
= DLOpenFailed !FilePath !String
-- ^ @dlopen@ returned null while opening this path.
| DLSymFailed !String !String
-- ^ @dlsym@ returned null for this symbol name.
deriving (Eq, Show)
instance Exception DLError
-- | Open a shared library for the duration of the callback and close
-- it on exit. Throws 'DLOpenFailed' if the library cannot be
-- loaded.
withLibrary :: FilePath -> (Library -> IO a) -> IO a
withLibrary path = bracket (open path) close
where
open p = do
h <- withCString p c_dlopen
if h == nullPtr
then do
!msg <- readErr
throwIO (DLOpenFailed p msg)
else pure (Library h)
close (Library h) = do
!_ <- c_dlclose h
pure ()
-- | Resolve a symbol as a @void(uint8_t *)@ target action. Throws
-- 'DLSymFailed' if the symbol is absent from the library.
--
-- The resolved 'FunPtr' is not retained past the enclosing
-- 'withLibrary' scope; do not call the returned action after the
-- library has been closed.
resolveTarget :: Library -> String -> IO (Ptr Word8 -> IO ())
resolveTarget (Library h) name = do
p <- withCString name (c_dlsym h)
if p == nullPtr
then do
!msg <- readErr
throwIO (DLSymFailed name msg)
else pure $! mkTargetFun (castPtrToFunPtr p)
readErr :: IO String
readErr = do
s <- c_dlerror
if s == nullPtr then pure "(unknown)" else peekCString s