How does purity create security boundaries?

← Security and Sanitization · Ref: Q273

EK9 pragmatic purity is focused on preventing mutation-based attacks and data corruption. It is not Haskell-style 'no side effects' but a practical security boundary.

WHAT PURE PREVENTS

Mutation operators are forbidden in pure functions:

  += -= *= /= :=: :~: :^:

These modify the left-hand side in place and could affect the right-hand side as a side effect. Calling user-defined impure functions is also blocked.

WHAT PURE ALLOWS

Reassignment with : (creates new value):

  result: result * i

Loop variables naturally change:

  for i in 1 ... n

Return variables are reassigned, not mutated. ALL I/O is allowed because I/O types carry the IO marker trait. Stdout, Stderr, Stdin, EnvVars, TextFile, TCP, and UDP are all usable in pure functions.

DATA INTEGRITY

The key insight is that + creates a new value while += mutates in place. In pure functions, only + is allowed. This means parameters passed to a pure function cannot be modified as a side effect:

  pureProcess() as pure
    -> data as String
    result <- "Prefix: " + data
    // 'data' is guaranteed untouched

DEFENSIVE COPY ENFORCEMENT

With sanitized parameters in pure context, ONLY the copy constructor works:

  secureProcess() as pure
    -> input as sanitized String
    localCopy <- String(input)
    // Must use localCopy, cannot mutate input

Purity and sanitization together force the safest possible handling.

CONTROLLED CALL CHAINS

Pure functions can only call other pure functions. This creates a trust chain: if the entry point is pure, every function in the chain is pure. No impure operation can hide in the middle.

TOCTOU PREVENTION

Time-of-check-time-of-use attacks require mutating shared state between the check and the use. Since mutation operators are blocked in pure functions, the checked value cannot change before it is used.

CONSUMER vs ACCEPTOR
Consumer of T is pure (read-only access). Acceptor of T is impure (can mutate). This distinction lets APIs express whether a callback can modify data:

  process()
    -> handler as Consumer of String
    // handler CANNOT modify anything

See Q54 for pure functions and Consumer/Acceptor. See Q217 for sanitized parameter mechanics in pure context. See Q218 for security best practices. See Q241 for mutation operators. See Q272 for defense in depth. See Q560 for purity contracts and enforcement rules. See Q635 for forbidden mutation operators in pure methods.

Example

defines module qa.security.puritysecurity

  defines function

    // Pure function with I/O — allowed
    pureWithIo() as pure
      -> message as String
      stdout <- Stdout()
      stdout.println("Pure I/O: " + message)

    // Pure function — reassignment, not mutation
    pureCalculate() as pure
      -> n as Integer
      <- result as Integer: 1

      for i in 1 ... n
        // result: result * i — reassignment with new value
        // result *= i — would be BLOCKED (mutation operator)
        result: result * i

    // Pure + sanitized = forced copy pattern
    pureSecureProcess() as pure
      -> input as sanitized String
      <- result as String?

      // Only copy constructor works here
      localCopy <- String(input)
      result: "Secure: " + localCopy

    // Consumer is pure — read only
    pureConsumerExample() as pure
      -> handler as Consumer of String
      handler("data")

  defines program

    PuritySecurityDemo()
      stdout <- Stdout()

      // === PURE WITH I/O ===

      pureWithIo("I/O is allowed in pure functions")

      // === PURE CALCULATION ===

      factorial <- pureCalculate(5)
      stdout.println("5! = " + $factorial)

      // === PURE + SANITIZED ===

      secureResult <- pureSecureProcess("external input")
      if secureResult?
        stdout.println(secureResult)

      // === CONSUMER vs ACCEPTOR ===

      stdout.println("Consumer of T: pure, read-only")
      stdout.println("Acceptor of T: impure, can mutate")

      // === SECURITY BENEFITS ===

      stdout.println("Purity security benefits:")
      stdout.println("  No mutation operators in pure")
      stdout.println("  Parameters cannot be modified as side effect")
      stdout.println("  Forced defensive copies with sanitized")
      stdout.println("  Pure call chains create trust boundaries")
      stdout.println("  TOCTOU prevented by no shared mutation")

Common mistakes

E08120 — In a pure function, the mutation operator *= is forbidden. Use reassignment with : to create a new value (result: result * i). See ek9 -h E08120 for details.

Incorrect:

result *= i

Correct:

result: result * i

E50001 — Renaming the variable means later references to 'factorial' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

factorialXYZ <- pureCalculate(5)

Correct:

factorial <- pureCalculate(5)

E50001 — Renaming the variable means later references to 'secureResult' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

secureResultXYZ <- pureSecureProcess("external input")

Correct:

secureResult <- pureSecureProcess("external input")
Other ways to ask this
  • Why does purity matter for security in EK9?
  • How does EK9 pragmatic purity prevent security bugs?
  • How do pure functions protect against data corruption?

Coming from another language?

Java: no purity concept, any method can mutate any reachable object. Python: no purity enforcement, convention only. Haskell: strict purity but no I/O in pure (uses IO monad). Rust: immutable borrows prevent mutation but no purity concept. Go: no purity. Kotlin: no purity enforcement. EK9: pragmatic purity blocks mutation operators while allowing I/O, creating security boundaries that prevent data corruption and TOCTOU attacks.

Keywords: protect, safe, integrity, migrate, data, mutation, side-effect, operator, immutable, purity, security, define, audit, pure, pragmatic, toctou