Security Review
A statically-typed, compiled language with compile-time safety guarantees and runtime protections
Executive Summary
Our GLaDOS language implementation prioritizes compile-time safety to prevent entire classes of bugs that plague C programs. We enforce strict type checking, initialization tracking, and control flow verification before any code runs, resulting in zero runtime overhead for these safety features.
Security Features Overview
| Safety Feature | Implementation | Detection Time | Status |
|---|---|---|---|
| Static Type System | Full type checking with inference | Compile-time | ✓ Implemented |
| Initialization Tracking | Flow-sensitive analysis | Compile-time | ✓ Implemented |
| Division by Zero (literals) | AST analysis | Compile-time | ✓ Implemented |
| Return Path Verification | Control flow analysis | Compile-time | ✓ Implemented |
| Type Inference | var keyword with initialization | Compile-time | ✓ Implemented |
| Stack Safety | VM bounds checking | Runtime | ✓ Implemented |
| Function Call Safety | Argument count & type validation | Compile-time | ✓ Implemented |
The Problem: Unsafe C Behaviors
1. Type Confusion
// C allows this - silent truncation
int x = 3.7; // becomes 3, no warning
char* p = (char*)123; // arbitrary casting
2. Uninitialized Variables
// C allows this - undefined behavior
int x;
printf("%d", x); // Could print anything
3. Division by Zero
// C allows this - runtime crash
int result = 10 / 0;
4. Missing Return Statements
// C allows this - returns garbage
int calculate(int x) {
if (x > 0) return x * 2;
// Oops, forgot the else case
}
Our Solutions
1. Strong Static Type System
What we do: Enforce type correctness at compile time with no implicit conversions.
Example - Type Mismatch Detection:
int x = true; // Our language
Error:
Type error at line 1:5
expected: int
got: bool
Example - Safe Type Inference:
var x = 42; // Inferred as int
var y = 3.14; // Inferred as float
var z = x + y; // Error: cannot add int and float
2. Initialization Tracking
What we do: Track variable initialization through control flow paths using a flow-sensitive analysis.
Example:
int x;
if (condition) {
x = 10;
}
return x; // Error: x may not be initialized
Error:
Variable 'x' used before initialization at line 4:8
Safe Pattern:
int x = 0; // Explicit initialization
if (condition) {
x = 10;
}
return x; // OK: x is definitely initialized
3. Division by Zero Detection
What we do: Catch literal zero denominators at compile time.
Example:
int y = 10 / 0;
float z = 3.14 / 0.0;
Error:
Division by zero at line 1:9
Runtime Protection:
int divisor = getUserInput();
int result = 10 / divisor; // Runtime check in VM
4. Return Path Verification
What we do: Ensure all control paths in non-void functions return a value.
Example:
int calculate(int x) {
if (x > 0) {
return x * 2;
}
// Missing else branch
}
Error:
Function 'calculate' must return a value on all code paths
Correct Version:
int calculate(int x) {
if (x > 0) {
return x * 2;
} else {
return 0;
}
}
5. Function Call Safety
What we do: Validate argument counts and types at compile time.
Example:
int add(int a, int b) {
return a + b;
}
int main() {
int x = add(1); // Error: wrong arg count
int y = add(1, true); // Error: type mismatch
return 0;
}
Implementation Architecture
Compile-Time Checks (Zero Runtime Cost)
- Parser (
Parser/Core.hs): Builds AST with source positions for error reporting - Type Inference (
Security/TypeInference.hs): Resolvesvardeclarations - Type Checker (
Security/TypeChecker.hs): Validates types and initialization - Compiler (
Compiler/Core.hs): Generates safe bytecode only for valid programs
Runtime Safety (VM Layer)
- Stack Bounds Checking (
VM/HelperFunc.hs): Prevents stack overflow/underflow - Local Variable Bounds (
VM/HelperFunc.hs): Array bounds checking for locals - Division by Zero (
VM/InstructionHandlers.hs): Runtime check for dynamic values - Type-Safe Operations: VM maintains type tags (VInt, VBool, VFloat)
Design Rationale
Why Compile-Time Over Runtime?
- Zero Performance Overhead: No runtime checks for proven-safe code
- Early Error Detection: Bugs caught before deployment
- Better Developer Experience: Immediate feedback during development
- Predictable Performance: No hidden runtime costs
Why Not Full Dependent Types?
We deliberately chose a practical middle ground:
- What we have: Strong static types with inference
- What we don't: Full dependent types or theorem proving
- Rationale: Balances safety with usability and compilation speed
Security Model Comparison
| Feature | C | Rust | Our Language |
|---|---|---|---|
| Type Safety | Weak, allows casting | Strong with lifetimes | Strong, no unsafe casts |
| Memory Safety | Manual, error-prone | Borrow checker | No pointers (safe by design) |
| Initialization | Undefined behavior | Enforced | Compile-time tracked |
| Null Safety | Null pointers | Option type | No null (no pointers) |
| Integer Overflow | Silent wraparound | Panic in debug | Silent (same as C) |
| Division by Zero | Runtime crash | Panic | Compile-time for literals, runtime otherwise |
| Return Checking | Undefined behavior | Enforced | Compile-time verified |
Limitations and Trade-offs
What We DON'T Prevent:
- Integer Overflow: Would require either runtime checks (performance cost) or different arithmetic semantics
- Runtime Division by Zero: Only catch literal zeros; dynamic values checked at runtime
- Infinite Loops: Halting problem - theoretically impossible to solve completely
- Stack Overflow from Deep Recursion: Would need tail-call optimization (future work)
Design Trade-offs:
- No Implicit Conversions: More verbose but prevents subtle bugs
- Mandatory Initialization: Requires explicit defaults but prevents undefined behavior
- No Null/Pointers: Limits expressiveness but eliminates entire bug classes
Testing Our Security
Type System Tests
// All should fail compilation:
int x = 3.14; // float to int
bool b = 42; // int to bool
int y = true + false; // bool arithmetic
Initialization Tests
// Should fail:
int x;
int y = x + 1; // x not initialized
// Should pass:
int a = 0;
int b = a + 1; // a is initialized
Control Flow Tests
// Should fail:
int risky(bool flag) {
if (flag) return 42;
// missing else
}
// Should pass:
int safe(bool flag) {
if (flag) return 42;
else return 0;
}
Future Security Enhancements
Planned Improvements:
- Tail Call Optimization: Prevent stack overflow in recursive functions
- Bounds-Checked Arrays: Safe indexing when arrays are added
- Pattern Matching: Exhaustiveness checking for safer control flow
- Effect System: Track side effects for pure functional programming
Considered but Deferred:
- Gradual Typing: Allow mixing static and dynamic typing
- Linear Types: Resource management without GC
- Formal Verification: Mathematical proofs of correctness
Conclusion
Our language prevents entire classes of bugs that plague C programs through a carefully designed type system and compile-time analysis. By catching errors before runtime, we provide both safety and performance. While we don't catch everything (that would be impossible), we focus on high-impact, provably detectable errors that cause real problems in production systems.
The key insight: Most bugs can be prevented by design, not just detected at runtime.