Lowent Manual←↑→

16 Capabilities — power that is handed over

What to know first

chapter 2, A first program · printing needs input out cap io .
chapter 15, Effects · effects are a closed set of atoms that spread along calls

Looking back

Chapter 15 said the effects line writes what an op does. What, then, can effects io alone not tell you?

A. What the input/output is with. Writing to standard output, opening a file and opening a connection are not told apart by the single word io. The capabilities of this chapter narrow that down — an op that received input fs cap file_system . can open files but not connections.

The need for this chapter, and its context

In most languages any function can write to standard output, open files and read the clock. That power is scattered globally, so finding out “does this library touch the network?” means reading all its source. Lowent has no such ambient authority. Power is handed over as an argument, like a value. Effects (chapter 15) and capabilities write the same thing from two sides and check each other, and with this chapter the three pillars of Part IV (contracts, effects, capabilities) stand.

By the end of this chapter

You will learn the kinds of capability and the work each opens. You will pick up that capabilities are passed down call chains by name, and that looking at the entry point alone tells you what outside a program can reach. You will see the table pairing effects with capabilities, and the diagnostics for declaring an effect without a capability, handing over the wrong kind, and using a capability without writing it. You will also understand which capabilities an entry point may receive, and that capabilities cost nothing at run time.

The questions this chapter answers

  1. Passing capabilities as arguments — doesn’t that add an argument per call at run time and slow things down?

16.1 Kinds of capability#

A capability is received as input <name> cap <kind> .. The kind decides what can be done with it.

CapabilityWhat it opens
cap ioStandard input and output
cap file_systemFiles and directories
cap netMaking connections and exchanging data
cap ttyTerminal size and modes
cap clockAsking the time
cap randomOperating-system entropy
cap args · cap envProgram arguments · environment variables
cap allocatorObtaining memory from a fixed window (does not grow)
cap heapObtaining memory from a growing root (only on machines with an operating system)
cap atomicAtomic operations
cap mmioDevice registers
cap cCalling C functions
cap machineEmitting machine instructions directly

Table 16.1 — Capability kinds the processor gives meaning to

The reason for kinds is least privilege. With only one capability, the permission received to read files could also open connections, and the op’s head would no longer say “what this op can do”.

16.2 Capabilities travel down the chain#

Passing a capability on needs no special notation. Write the received name like any other argument.

examples/ch16/chain.low

module chain .
rem run: main

proc say input k cap io . input msg slice u8 . output u64 . effects io .
do
  return write_out k 1 msg .
end

proc say_twice input k cap io . input msg slice u8 . output u64 . effects io .
do
  let a u64 be say k msg .
  let b u64 be say k msg .
  return add a b .
end

proc main input k cap io . output u8 . effects io .
do
  let n u64 be say_twice k "hi\n" .
  guard eq n 6 . else return 1 .
  return 0 .
end

Output

$ lowentc --run main chain.low
hi
hi
main() = 0

The chain looks like this — outside → main’s k → say_twice’s k → say’s k → write_out. Drawn together with effects, the two arrows run in opposite directions.

                  capability (permission)          effect (what was done)
                  handed down from the top         spreads up from the bottom
  outside (OS)
      │ cap io
      ▼
  main       k ─────────────┐                  ▲ effects io
      │                     │                  │
      ▼                     ▼                  │
  say_twice  k ─────────────┐                  ▲ effects io
      │                     │                  │
      ▼                     ▼                  │
  say        k ─────▶ write_out k 1 msg        ▲ effects io   ← io arises here

A capability must be handed down from above to be used; an effect arises below and is written in the head of every caller. So the head of any op shows both “what this op can do (effects)” and “who allowed it (capabilities)”. An op that received no capability cannot call say, because it has nothing to pass. So “can this program touch files?” can be answered by looking only at the entry point. No op deep inside can secretly open a file: without the capability it cannot, and with it, the chain is visible all the way back to the entry.

Q. Passing capabilities as arguments — doesn’t that add an argument per call at run time and slow things down?

A. It does not. A capability is a mark at translation time, not a run-time value. Ops that receive capabilities and ops that do not have the same signature at the C boundary. A capability asks “is this op allowed to be called?” at translation time, and nothing is left to ask at run time. The value that guards the boundary does not come back as a run-time cost.

16.3 Effects and capabilities are a pair#

The effects line writes what an op does; capability inputs write who allowed it. The same thing is written from two sides, and each checks the other.

EffectAuthorising capabilityWhen missing
iocap io · file_system · net · tty · clock · randomE-EFFECT-NO-CAP
alloccap allocator (or a region)E-ALLOC-NOCAP
heapcap heapE-HEAP-NOCAP
atomiccap atomicE-ATOMIC-NOCAP
devicecap mmioE-MMIO-NOCAP
state · panic · wait · concurrentwritten without a capability—

Table 16.2 — Effects and the capabilities that authorise them

examples/ch16/pairs.low

module pairs .
rem run: main

proc main input out cap io . input al cap allocator . output u8 . effects io alloc .
do
  let g option mut slice u8 be alloc_bytes al capacity 16 .
  guard is_some g . else return 1 .
  let n u64 be write_out out 1 "hi\n" .
  return 0 .
end

Output

$ lowentc --run main pairs.low
hi
main() = 0

main receives two capabilities. cap io authorises effects io, and cap allocator authorises effects alloc. alloc_bytes al capacity 16 asks the allocator for 16 bytes and gives an option, since it may fail.

There are three ways the pair can go wrong. Declaring an effect with no authorising capability is rejected.

examples/ch16/nocap.low

module nocap .
rem expect: E-EFFECT-NO-CAP

proc main output u8 . effects io .
do
  let n u64 be write_out 1 "hello\n" .
  return narrow u8 n .
end

Output

$ lowentc --check nocap.low
nocap.low:4:0 E-EFFECT-NO-CAP: this op declares the `io` effect but receives NO capability that authorizes it. `io` is a cap-effect (RFC-0007 §6.7): I/O is a RIGHT you are HANDED, not an ambient power — `input fs cap file_system .` (or another `cap …` input). An effect you declare but hold no capability for is a claim the signature cannot back

Allocation is the same: it declares using memory but not where the memory comes from.

examples/ch16/alloc_nocap.low

module alloc_nocap .
rem expect: E-ALLOC-NOCAP

proc scratch output u8 . effects alloc .
do
  return 1 .
end

Output

$ lowentc --check alloc_nocap.low
alloc_nocap.low:4:0 E-ALLOC-NOCAP: this op declares the `alloc` effect but receives NO allocation capability. There is no ambient heap in this language (RFC-0043 D1): to allocate you must be HANDED the right — `input a cap allocator .` or `input r region <name> . .` (a region IS an arena allocator, RFC-0043 D7). An allocation nobody granted is exactly the hidden dependency this model removes
alloc_nocap.low:5:1 W-EFFECT-OVER: this op DECLARES `alloc` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

Receiving a capability and never handing it to the place that uses it is rejected too. A builtin op that requires a capability takes it as its first operand.

examples/ch16/missing.low

module missing .
rem expect: E-CAP-MISSING

proc hello input out cap io . output u64 . effects io .
do
  return write_out 1 "hello\n" .
end

Output

$ lowentc --check missing.low
missing.low:6:0 E-CAP-MISSING: `write_out` writes to the process output and needs the `cap io` value the entry received as its FIRST operand — output is a RIGHT you are handed (RFC-0030 D2), never ambient. There is no hidden stdout

There is no way to reach a capability without naming it — in other words, there is no hidden standard output.

16.4 Kind, not presence#

Holding some capability does not open every door.

examples/ch16/kind.low

module kind .
rem expect: E-CAP-KIND

proc stamp input out cap io . output u64 . effects none .
do
  return time_now out .
end

Output

$ lowentc --check kind.low
kind.low:6:0 E-CAP-KIND: this host leaf was handed a capability of the WRONG KIND. Authority is by KIND, not by mere presence: an unrelated right (io, net, …) does not authorize the clock, the filesystem, the socket or the OS entropy (RFC-0083 L3 · RFC-0077 §P1-2). Take the right this leaf names — the FFI boundary has always been checked this way (E-FFI-CAPKIND); the host leaves used to be refused only at lowering, where a WRONG RIGHT was reported as an UNSUPPORTED FEATURE

time_now is the primitive that reads the clock. A cap io was handed over, so it is rejected as the wrong kind. Capabilities authorise by what they are.

A common misconception. Reading the time with cap clock requires declaring the io effect

The standard library’s clock.now_ms takes a cap clock but is effects none. A clock breaks determinism, so it needs a capability, but it leaves no trace outside. Capabilities and effects are not always one-to-one. The io row of the pair table is a rule in one direction: “to perform the io effect you must receive one of these”.

Authors can name capabilities of their own. Receiving your own capability as a parameter, as in input logger cap audit ., lets you build ops that only code holding that capability can call. What the processor gives meaning to are the kinds in the table above, and work of those kinds requires those kinds.

16.5 What the entry point receives#

The op a program starts from receives only capabilities. It does not receive data, because nobody is there to hand it over.

examples/ch16/entry.low

module entry .
rem expect: E-ENTRY-PARAMS

proc main input out cap io . input n u64 . output u8 . effects io .
do
  return narrow u8 n .
end

Output

$ lowentc --check entry.low
entry.low:4:0 E-ENTRY-PARAMS: the entry point takes CAPABILITY inputs only (`input a cap args .`). A data parameter here would be a magic argv — RFC-0030 D2 rejects that: arguments are a RIGHT the runtime hands you, queried via `count`/`arg` on the cap
entry.low:5:1 W-EFFECT-OVER: this op DECLARES `io` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

There is no magic argument like C’s argv; program arguments are received through the cap args capability too. The capabilities an entry point may receive form a closed list — args, env, io, allocator, heap, file_system, net, tty, clock, random, atomic. Other capabilities (mmio, machine, c) are not something the executing environment can hand over, so an entry point requiring them is rejected with E-ENTRY-CAP. Such capabilities must be created where someone is entitled to grant them and flow in as arguments (chapters 29 and 30).

In practice. Finding out whether a dependency reaches the network

What worries you most when bringing in someone else’s library is what that code touches. In Lowent you look through the heads of the library’s exported ops for any that take cap net (also look for cap c, which escapes into C, and cap machine, for machine code — the processor cannot see inside those). If there are none, the library cannot open connections. If there are, you must hand over cap net where you call them, so that place is the one to audit. A common supply-chain attack — imported code quietly connecting outward — becomes visible in the grammar.

Environment variables and operating-system randomness are also received as capabilities.

examples/ch16/envrandom.low

module envrandom .
rem run: main

use random .

rem environment variables need cap env and the operating system's entropy needs cap random; both must be handed over
proc main input e cap env . input r cap random . input al cap allocator . output u8 . effects alloc .
do
  let g option mut slice u8 . . be alloc_bytes al capacity 8 .
  guard is_some g . else return 250 .
  var buf mut slice u8 . be some_value g .
  rem fill eight bytes with operating-system randomness; the value differs every run, so it is not returned
  let n u64 be random.bytes r buf .
  rem a missing variable gives none, an absence rather than a value
  let v option slice u8 be env_get e "LOWENT_MANUAL_DEMO" .
  guard is_some v . else return narrow u8 n .
  return 99 .
end

Output

$ lowentc --run main envrandom.low
main() = 8

env_get e "…" needs cap env, and random.bytes r buf needs cap random. An environment variable is a channel that quietly changes behaviour from outside the program, and randomness is a channel that changes the answer for the same input. Both must be visible in the head so that the head alone tells whether “this program’s answer is decided by its inputs”. With no variable, env_get gives none and the example returns 8, the number of bytes filled. When tests need repeatable random numbers, use rng_next (the next number from a seed), which needs no capability.

16.6 Common mistakes#

Counter-example. Thinking a fn may print because it received a capability

examples/ch16/mistake_fncap.low

module mistake_fncap .
rem expect: E-EFFECT-CALC

rem ✘ receiving a capability does not let a `fn` produce output
fn say input out cap io . output u64 .
do
  return write_out out 1 "hi\n" .
end

Output

$ lowentc --check mistake_fncap.low
mistake_fncap.low:6:1 E-EFFECT-CALC: this fn is declared pure but performs `io` — make it a `proc` with `effects …`, or remove the effect

A capability says “who allowed it”; an effect says “what it does”. Neither stands in for the other. Even with cap io, printing is the io effect, and a pure fn cannot perform effects, so this is E-EFFECT-CALC. Change it to proc say … effects io .. Both the permission (capability) and the action (effect) must be in the head, so that each checks the other.

Counter-example. Moving a received capability into a local name

examples/ch16/mistake_caplocal.low

module mistake_caplocal .
rem expect: E-CAP-LOCAL

proc say input out cap io . output u64 . effects io .
do
  rem ✘ the capability is moved into a local name --- pass the received name directly
  let k cap io be out .
  return write_out k 1 "hi\n" .
end

Output

$ lowentc --check mistake_caplocal.low
mistake_caplocal.low:8:0 E-CAP-LOCAL: `k` is a LOCAL that a `cap io` was copied into. A capability is not a value you carry in a local name: only a parameter of this op names a right it was handed, so hand the parameter itself to the leaf (RFC-0030 D2)

A capability is passed down the chain under the name it was received with. Moving it into a local name makes the chain look broken, so it is rejected with E-CAP-LOCAL; the message names the local and the kind that was copied into it. Write the parameter name directly, as in write_out out 1 ….

Counter-example. Writing to standard output with a capability of another kind

examples/ch16/mistake_fscapout.low

module mistake_fscapout .
rem expect: E-CAP-KIND

rem ✘ tries to write to standard output with the file capability
proc main input fs cap file_system . output u8 . effects io .
do
  let n u64 be write_out fs 1 "hi\n" .
  return 0 .
end

Output

$ lowentc --check mistake_fscapout.low
mistake_fscapout.low:7:0 E-CAP-KIND: this leaf needs a `cap io`, and `fs` is a `cap file_system`. One capability never stands in for another: each names a different right, and holding one says nothing about the other (RFC-0011)

cap file_system opens files and directories; it is not the capability for standard output. Capabilities are authorised by kind, not by possession, so this is rejected with E-CAP-KIND, which prints the kind required beside the kind handed over. The fix: the entry receives input out cap io . and passes that name.

Counter-example. Passing a number where a capability belongs

examples/ch16/mistake_capforge.low

module mistake_capforge .
rem expect: E-CAP-FORGE

proc say input k cap io . output u64 . effects io .
do
  return write_out k 1 "hi\n" .
end

rem ✘ receives no capability at all, yet prints by passing the number 0 where a capability belongs
proc start output u8 .
do
  let n u64 be say 0 .
  return 0 .
end

Output

$ lowentc --check mistake_capforge.low
mistake_capforge.low:12:0 E-CAP-FORGE: a LITERAL was passed where a capability is taken. A capability is handed over, never conjured: it can only be a name you were given (an `input … cap …`) or an actor's capability field (RFC-0030 D2). Passing a number here would let an op that received NO right reach the outside, and then the entry point no longer tells what the program can touch. The placeholder `0` means something only at the tool's door (`--run`)

start receives no capability at all, so it should not be able to print. Passing the number 0 in the cap io position of say looks like a way around that rule, and it is refused with E-CAP-FORGE. Only a name you were handed (input … cap …) or an actor’s capability field may stand in a capability position. Without that, the promise of this chapter — “the entry point tells you everything the program can reach” — would fall to a single line. The placeholder 0 that --run uses means something only at the tool’s door (chapter 31).

A common misconception. It is convenient to take capabilities and effects in advance, in case they are needed later

examples/ch16/capjustincase.low

module capjustincase .
rem expect: W-EFFECT-OVER

rem the capability and the effect are declared just in case they are needed later
proc area input out cap io . input w u64 . input h u64 . output u64 . effects io .
  requires le w 1000 .
  requires le h 1000 .
do
  return mul w h .
end

Output

$ lowentc --check capjustincase.low
capjustincase.low:8:1 W-EFFECT-OVER: this op DECLARES `io` but never PERFORMS it. A declared effect is a cost the caller must budget for (a pure caller cannot call an `io` op). Drop it, or — if a future version will perform it — say so (RFC-0007)

area only multiplies, yet it declares cap io and effects io. Now every caller must find a cap io to pass, and pure code cannot call it at all. The tool warns with W-EFFECT-OVER. Take as few capabilities as possible — the list of received capabilities is “what this op can reach”, so a generous list tells a lie.

16.7 This chapter’s syntax at a glance#

ShapeMeaningWhy
input out cap io .receive the standard I/O capabilityno ambient authority — power is handed in as an argument
input fs cap file_system . · cap net · cap clock …each kind opens different thingsleast authority — the head says what it can reach
say k msgpass a received capability name as isthe chain is visible up to the entry point
write_out out 1 "…"builtins that use a capability take it as the first operandno way to reach a capability without a name
effects io + cap ioan effect and the capability that allows itmissing: E-EFFECT-NO-CAP
effects alloc + cap allocatorthe pair for fixed-window allocationmissing: E-ALLOC-NOCAP
proc main input … cap … . output u8 .the entry point receives only capabilitiesnobody can hand it data
input logger cap audit .a capability named by the authoronly ops that received it can call the guarded op
env_get e "…" · random.bytes r bufenvironment variable · OS randomness — cap env · cap randomoutside channels that change the answer show in the head

Table 16.3 — Capability syntax — shape · meaning · why it looks this way

Recap

Capabilities are handed over as input <name> cap <kind> ., and the received name is passed on as is. Looking at the entry point shows what outside a program can reach. Effects and capabilities are a pair: declaring an effect requires receiving an authorising capability, and builtin ops that use a capability take it as their first operand. Capabilities authorise by kind and cost nothing at run time. The entry point receives only capabilities from a closed list.