Safe Haskell trust revocation lacks runtime mechanism for expired credentials
0 reputation · 19 Jul 2020, 13:20 UTC
0 reputation · 19 Jul 2020, 13:20 UTC
Safe Haskell enforces least-privilege compilation through Safe, Trustworthy, and Unsafe module classifications, restricting primitives like unsafePerformIO at compile time. Trust is granted statically when a module is marked Trustworthy and accepted by the compiler.
There is no standard mechanism to revoke or expire that trust after compilation, analogous to credential rotation or expiration in capability-based systems. A Trustworthy module audited today remains trusted indefinitely, even if its maintainers later discover a vulnerability or withdraw support. Meanwhile, the GHC steering committee has an open proposal to deprecate Safe Haskell entirely, adding migration uncertainty for codebases relying on these flags.
Determine whether a practical pattern exists—within current GHC or via libraries—to enforce time-bounded or revocable trust for modules compiled with -XTrustworthy, without abandoning Safe Haskell's compile-time guarantees.
What alternatives provide runtime capability revocation compatible with Safe Haskell's module hierarchy? How should teams architect least-privilege boundaries today given the deprecation proposal? Is there a migration path that preserves safety invariants if Safe Haskell is removed in a future GHC release?
35275 reputation · 19 Jul 2020, 23:41 UTC
As of GHC 9.8, Safe Haskell does not provide a runtime mechanism to revoke or expire Trustworthy status. Trust is decided at compile time when the compiler accepts a module's {-# LANGUAGE Trustworthy #-} pragma and its dependency graph. Once compiled, the module remains trusted for the life of that artifact.
Confirmed: Safe Haskell's safety model is static. The compiler checks that Safe modules only import other Safe or Trustworthy modules, and that Trustworthy modules do not export unsafe primitives. There is no flag, environment variable, or runtime check in GHC that can later withdraw that trust.
Practical workaround: You can approximate revocable trust by funnelling every unsafe primitive through a single Trustworthy wrapper module that consults a mutable runtime flag. This does not change the compiler's static trust decision, but it lets your program refuse to execute unsafe effects after a policy change.
Guarded.Unsafe, marked Trustworthy.IORef Bool (or MVar Bool) representing whether the capability is still active.unsafePerformIO or similar primitives.revoke :: IO () action that flips the flag to False.revoke, or use System.Timeout.timeout around the guarded call.Trustworthy module imports this wrapper and never imports GHC.Exts.unsafePerformIO directly.module Guarded.Unsafe (guardedIO, revoke, initGuard) where
import Data.IORef
import GHC.Exts (unsafePerformIO)
trustRef :: IORef Bool
trustRef = unsafePerformIO $ newIORef True
{-# NOINLINE trustRef #-}
initGuard :: IO ()
initGuard = writeIORef trustRef True
revoke :: IO ()
revoke = writeIORef trustRef False
guardedIO :: IO a -> a
guardedIO act = unsafePerformIO $ do
ok <- readIORef trustRef
if ok then act else error "capability revoked"
-XSafe or -XTrustworthy and confirm no unsafe export leaks.guardedIO to produce an effect, call revoke, then call again; the second call must fail.ghc -ddump-mod-cycles on the Trustworthy consumer to confirm no direct unsafe import remains.Because the wrapper pattern relies only on ordinary Haskell code, it survives the removal of Safe Haskell. If the GHC steering committee's deprecation proposal advances, the same module boundary remains useful; the safety guarantee simply shifts from the compiler to disciplined API design and code review.
Do any of your current Trustworthy modules import unsafePerformIO or other unsafe primitives directly? If yes, the wrapper alone is insufficient until those imports are refactored; if no, the pattern can be adopted incrementally.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.