Security guarantees of compile-time sandboxing

Jo proves before execution that a program can only use explicitly granted authority. The sandbox is checked by the compiler, not bolted around a running process.

The Core Idea

What is Compile-Time Sandboxing?

Jo implements compile-time sandboxing with capability-based typing. The mechanism builds on object-capability security research with a long academic history, from Lampson's confinement problem (1973) to Mark Miller's E language, David Wagner et al's Joe-E, and recent research on contextual capabilities.

compile-time sandboxing=API gating in the language reflection globals network files type cast control effects Confined function def work(mailer: Mailer, logger: Logger) = ... interface Mailer interface Logger

Function parameters (capabilities) are the only door to the outside world.

Compile-time sandboxing is not yet a standard practice. Jo establishes it while the ecosystem is still consolidating around runtime sandboxing.

What compile-time sandboxing guarantees

If untrusted code type-checks, it can only use the authority explicitly passed to it. Anything outside that contract is rejected before deployment.

No unauthorized I/O

All side effects require explicit authority. Untrusted code cannot access filesystem, network, or other resources unless the interface grants that access.

No privilege expansion

Narrow authority cannot recover broader access. A user-scoped DB interface cannot be turned into full DB access by confined code.

No language-level escape hatches

No reflection, no ambient state, no FFI in untrusted code. The type system enforces the sandbox boundary.

Timing side-channel attacks

Spectre-class attacks exploit speculative execution to read memory across a privilege boundary via cache timing.

Jo's compile-time sandboxing provides meaningful protection here: the attack requires a high-resolution timer (or performance counters) to measure cache access times, and timer access must be explicitly granted to untrusted code. Without timer access, the timing channel cannot be read and the attack cannot proceed.

Most processor vendors and operating systems have also shipped firmware updates and kernel mitigations that significantly reduce the attack surface.

How Spectre attacks work

The CPU speculatively executes instructions ahead of branch resolution to improve performance. The attack uses a bounds-check guard as the target: by repeatedly calling with valid indices, the attacker trains the branch predictor to expect index < array.length to be true. Then passing an out-of-bounds index causes the CPU to speculatively execute the body — reading beyond the array into memory which should be inaccessible — before the check resolves and the branch is rolled back.

The CPU rolls back before the program ever sees the value. But before the speculative read, the attacker sets up a 256-page probe array. The speculative path uses the secret byte as an index into it:

if index < array.length:          # guard — attacker mistrains branch predictor to predict true
    secret = array[index]          # out-of-bounds, speculative — rolled back
    _ = probeArray[secret * 4096]  # encodes secret into which cache line is warmed

After rollback, secret is gone from any accessible variable. But one of the 256 probe pages is now in cache. The attacker times all 256 accesses — the fast one reveals the byte:

for i in 0..256:
    t0 = performance.now()
    _ = probeArray[i * 4096]
    t1 = performance.now()
    if t1 - t0 is fast: secret = i   # cache hit — this i is the secret byte

The value is never read directly — it is inferred entirely through which memory access is fast. This is the side channel.

Why Compile-Time Sandboxing?

Runtime sandboxing

Enforced in the infrastructure, after deployment

Compile-time sandboxing

Enforced by the compiler, before code runs

Jo

Blind to business logic

Can restrict filesystem and network behavior, but cannot express rules like "read only this user's rows" or "only these 5 narrowed REST APIs"

Aware of business logic

Rules like "read only this user's rows" or "only these 5 narrowed REST APIs" are typed capabilities the compiler enforces.

Boundary buried in the deployment stack

Authority is scattered across configs and runtime parameters — auditing means digging through infrastructure.

Boundary is typed, versioned code

Authority is declared in typed interfaces — auditing means reviewing code in version control.

Violations surface at runtime

Escapes are discovered at runtime, after the code is already deployed.

Violations are compile errors

The compiler pinpoints them in source, with detailed errors — before anything is deployed.

What the sandbox depends on

Jo's compile-time sandboxing guarantees hold under the following assumptions.

Correct type checker

The guarantees are as strong as the Jo type checker. Jo is open source and tested with an adversarial test suite that includes cases designed to violate capability contracts. Compiler bugs would weaken the guarantees.

Reviewed sandbox interface implementations

Trusted code that implements sandbox interfaces must correctly encode the intended access control rules — for example, scoping database access to the current user. Jo enforces the boundary at the interface; correctness of what is inside that boundary is a code review responsibility.

What compile-time sandboxing does not guarantee

Compile-time sandboxing proves authority boundaries, but it does not enforce resource limits or prevent misuse of granted authority.

Resource exhaustion

Jo does not prevent long-running programs or memory overuse. Use infrastructure-level controls (timeouts, memory limits) alongside Jo for deep defense if needed.

Implementation bug

Sandbox boundaries are only as strong as the trusted interface implementations. Business access control rules still require security review.

Misuse within scope

Untrusted code may misuse authority that was deliberately granted to it. Jo enforces the sandbox boundary; it cannot infer correct intent.

Defense in Depth

Jo does not ask you to choose between compile-time and runtime sandboxing. It adds a language-level authority boundary, then lets you pair it with the lower layers you already trust: from microVMs, to containers, Bubblewrap/landrun/firejail, or custom security stack.

Layer 1

Jo compile-time sandboxing

Rejects unauthorized authority in source before deployment. User-scoped data access, narrowed APIs, and application authority are expressed as typed capabilities.

Layer 2

Runtime isolation you trust

Restrict filesystem and process behavior with the technology that fits your deployment model: microVMs, containers, Bubblewrap/landrun/firejail.

Layer 3

Network policy you control

Limit network paths and egress with host or namespace-level controls such as nftables, independent of what a process attempts to open after it starts.

Defense in depth means no single layer has to be the only thing standing between an agent and sensitive infrastructure.

Responsible disclosure

To report a security vulnerability in Jo, use GitHub's private vulnerability reporting at github.com/typescope/jo/security.