9. The escape hatch is a door, not a cliff
"Come a little closer."— Cage the Elephant
The C library is twenty years old and it works. This chapter is about exactly how close we let it come.
The shelf has one job left that wolf will not be doing itself. Snapshots go out over a socket, and they go out packed, and the packing code was written before most of this book’s readers had a text editor. Nobody is rewriting it. So the shelf calls it:
import c "stdlib.h"
import c "string.h"
struct Doc { title: str, words: int }
fn main() -> !int {
let snapshot = Doc { title: "regions", words: 900 }
var packed = 0
// # Safety: both allocations own 64 bytes for the whole block,
// memset and memcpy stay inside them, and each is freed once.
unsafe {
let raw = c.malloc(64)
c.memset(raw, 0, 64)
let out = c.calloc(8, 8)
c.memcpy(out, raw, 64)
c.free(raw)
c.free(out)
packed = 64
}
print("{snapshot.title}: {snapshot.words} words, {packed} bytes out")
0
}
$ lupin snapshot.lu
regions: 900 words, 64 bytes out
Those are real malloc and free. Compile the same file and the
binary calls the ones in your libc, by those names, through the ABI your
platform already publishes:
$ wolf build snapshot.lu && ./snapshot
regions: 900 words, 64 bytes out
$ echo $?
0
Two chapters built a memory model with no escape in it. This one puts the escape in, and the surprise — the thing this chapter exists to say — is that the escape is not the dangerous end of the language. It is the simple end. Inside those braces the rules are C’s rules, which you already know, plus an oracle that answers the question C never could.
9.1 The three rings
The word unsafe is a bad name for what the braces do, and every
language that has the word knows it. The block does not make the code
unsafe. It marks the code where the compiler stops proving things and
you start.
Marking is the point. A ring is a place in the source where the proof obligation changes hands, and wolf has exactly three of them:
unsafe { }blocks. Raw pointers, calls into C, aliasing assertions, the doors back. Nothing in the raw tier happens anywhere else.#[trusted]functions. Code whose correctness rests on an invariant no checker can see, declared in the package manifest so that a dependency growing one is a diff and not a secret. §9.7.- Inline C and assembly. Ring 1 by construction: the syntax only
parses inside an
unsafeblock, so an audit that finds the blocks finds these too.
Three rings, and all three are one text search. That is not a slogan;
it is a property of the grammar. A raw pointer value is inert — you
can hold one, copy one, pass one around in ordinary safe code all day —
but every raw operation is inside a ring or it does not parse. So the
complete unsafe surface of a codebase is what unsafe finds, and the
complete FFI surface is what import c finds, and there is no fourth
place to look.
Run that search over everything this book has printed so far and the answer is zero: chapters 1 through 8 never left the safe tier, and the count is trustworthy in a way the same search is not trustworthy in C, where every pointer dereference in the program is potentially the audit’s subject. Exercise 9-1 does the search.
The compiler will also do the counting for you. wolf audit-surface
walks a package and reports, per module, the trusted functions with
their stated obligations and the number of unsafe blocks, aliasing
assertions, re-entry doors, C imports, and inline blocks in it. It is a
reviewer’s tool rather than a gate, and §9.7 is about what a reviewer
does with the number.
Exercise 9-1 (fingers + spelunking · wolf) — The complete unsafe audit of every program in this book’s first eight chapters is one command. Run it over the exercise corpus, report the number, and state what property of the language makes the count trustworthy — what would the same search miss in C?
9.2 Raw-tier rules
Here is the rule that decides everything else in this chapter, and it is
one sentence: a raw pointer *T carries no aliasing assumptions.
The implementation treats it the way a C compiler treats char*. Two
raw pointers may point at the same byte. A write through one may be seen
through the other. Pointer arithmetic runs off the end of one object and
into the next if you write it that way. Casting a pointer to an integer
and back gets you a usable pointer. None of that is a special dispensation;
it is the absence of a rule, which is what “no assumptions” means.
So the tier boundary is not about the values. It is about the operations:
import c "stdlib.h"
fn main() -> !int {
// # Safety: the allocation is eight bytes and lives until the free
// below; handing the pointer out of the block writes nothing.
let p = unsafe { c.malloc(8) as *u8 }
p[0] = 1
// # Safety: `p` is that same allocation, freed exactly once.
unsafe { c.free(p) }
0
}
error[E1301]: a raw pointer write needs an `unsafe` block
--> ./s2.lu:9:5
|
9 | p[0] = 1
| ^^^^ raw-tier operation in safe code
|
= note: raw pointers are inert data anywhere — only the tier's operations need the ring. Wrap
this in `unsafe { }` and state the invariant in a `# Safety:` comment; the rules inside
are simpler than the safe tier's, not stricter.
Read what the compiler did not complain about. The binding above the
error makes a raw pointer inside a block and hands it out into safe
code, and that is fine — an unsafe block is an expression of its
body’s type, so it yields a *u8 the way any block yields its last
value. The pointer then sits in a safe binding, gets copied, gets passed
to functions. Only the write is the tier’s business.
The note carries the other half of the ring’s deal, and it is a deal
worth keeping: every block states the invariant it maintains in a
# Safety: comment. The compiler asks for it by name and warns when it
is missing. The reason is §9.7’s, in one line: the block is where a
human takes over the proof, and a proof nobody wrote down is not one.
Unsafety does not cross a signature
There are no unsafe fns in wolf. A function’s signature is safe or the
program does not compile:
import c "stdlib.h"
fn header_len(p: *u8) -> int {
8
}
fn main() -> !int {
0
}
error[E1302]: `header_len`'s parameter `p` carries a raw pointer, but this boundary stays fully safe
--> ./s3.lu:5:15
|
5 | fn header_len(p: *u8) -> int {
| ^ `*T` crosses the boundary here
|
= note: unsafety never appears in types crossing function boundaries — there are no `unsafe
fn`s; the proof lives at the `unsafe` block and the module is the audit granule. Pass a
`handle` (revalidated per access) or a region value, or keep the `*T` in a
module-private field.
This is a design decision with consequences the rest of the chapter
lives on. A *T parameter would mean the caller has an obligation, and
a caller with an obligation has callers of its own, and the audit
surface becomes the call graph. Wolf puts the obligation where it can be
discharged: at the block, inside a module, behind a signature that
promises nothing about pointers. The module is the audit granule: an
unsafe block’s argument may lean on invariants its module’s private
items maintain, and on nothing wider.
Creation is not a use
One rule inside the raw tier is worth stating on its own, because it is where the aliasing models people know from elsewhere get it wrong. Making a second pointer to the same bytes is not an access:
import c "stdlib.h"
fn main() -> !int {
var out = 0
// # Safety: `p` and `q` address the same live eight bytes, every
// access is in bounds, and the allocation is freed exactly once.
unsafe {
let p = c.malloc(8) as *u8
p[0] = 1
let q = p
p[1] = 2
out = (q[0] + p[1]) as int
c.free(p)
}
print("{out}")
0
}
$ lupin siblings.lu
3
q is derived from p between two uses of p, and then both are read.
Nothing here is undefined, at either machine. Deriving a pointer touches
no permission; only accesses do.
Coming from Rust: this program is the litmus that separates the two published models of pointer aliasing, and wolf’s answer is the second one on purpose. Under Stacked Borrows — the first operational model for Rust’s aliasing rules, and a real contribution — creating a pointer pushes it onto a per-location stack, so using the parent afterward pops the child off and using the child later is undefined. The consequence is documented and was measured on Rust’s own standard library: a large share of the violations the model reported were programs doing exactly what the block above does, deriving a pointer and not using it yet. Tree Borrows, the successor model, keeps the tree and gives a freshly derived pointer a reserved state that survives its parent’s reads. Wolf’s machine is that one. The honest summary of the difference is not that Rust’s authors were careless — they were the people who noticed the problem exists — it is that an unsafe tier whose rules are stricter than the safe language is a tier where the standard library’s own authors get surprised, and wolf chose the tier where the rules are looser than the safe language instead. What replaces the strictness is §9.3.
The one assumption you can make
Since the tier assumes nothing about aliasing, the optimizer can assume
nothing either — and sometimes you know better than both. assume noalias is how you say so:
import c "stdlib.h"
fn main() -> !int {
var merged = 0
// # Safety: `left` and `right` are two distinct live allocations,
// so the assertion is true; both are freed exactly once.
unsafe {
let left = c.malloc(8) as *u8
let right = c.malloc(8) as *u8
assume noalias left, right
left[0] = 1
right[0] = 2
merged = (left[0] + right[0]) as int
c.free(left)
c.free(right)
}
print("{merged}")
0
}
$ lupin disjoint.lu
3
The assertion says the two ranges do not overlap for the rest of the
scope, and the code generator spends it: the asserted pointers get the
treatment vectorization and reordering need, as if the disjointness had
been proven. It is C’s restrict with two differences. It is a
statement, not a qualifier, so it appears at a line number rather than
in a type. And a false one is caught, which is §9.3.
That is the entire aliasing surface of the raw tier: no assumptions, one way to add one, and the addition is a line you can grep for.
Exercise 9-2 (fingers · lupin) — Your first unsafe block, kept legal: allocate eight bytes from C, set them all to 5, read one back, free, print. Type it, run it, and note the exit code — the point of this exercise is that nothing happens.
Exercise 9-3 (comprehension · lupin) — One character changes in
9-2: the write is p[8] = 1. The allocation holds eight bytes. Predict
the oracle’s finding — its row, and which optimizer license the report
will name.
Exercise 9-4 (comprehension · lupin) — The pointer is laundered
through an integer before the read: cast p to int, free the
allocation, cast the integer back to *u8, and read through the new
pointer. An integer survives free untouched. Does the roundtrip save
the read? Predict the oracle’s answer and its reasoning.
9.3 The oracle you actually run
Everything above traded a guarantee for a freedom. Here is what wolf gives back for it.
Here is an unsafe block that allocates, writes, frees — and then reads, which is the oldest bug in systems programming and one line out of order:
import c "stdlib.h"
fn main() -> !int {
// # Safety: DELIBERATELY WRONG — the read below happens after the
// free, which is the fault this section is about.
unsafe {
let p = c.malloc(8) as *u8
p[0] = 7
c.free(p)
let v = p[0]
v as int
}
}
$ lupin uaf.lu
uaf.lu: ub(mem.ub) §7/P1: read through tag#0 (c.malloc(8)#root), which is Disabled at alloc#0[0] [mem.prov.state] at 267..271; tag created at 197..208
licenses O1: `mut` params lower to `noalias` + `dereferenceable`; unique-tag stores forward without memory checks
alloc#0 `c.malloc(8)` 8 byte(s), FREED, owned by region #0
tag#0 c.malloc(8)#root Disabled exposed
$ echo $?
3
Run it again and you get those bytes again. Run it on another machine and you get those bytes again. This is the part that has no counterpart in C: a use-after-free in C reads whatever the allocator happened to leave behind, which is sometimes 7, sometimes the next object, sometimes a signal, varying with allocator, build flags, and how much else the program had done first. The report above is not a reading of memory. It is a reading of the rules, and rules do not vary between runs.
The compiler’s checked build reaches the same verdict from the other implementation, in its own voice:
error[E1401]: undefined behavior: [mem.ub] row P1 — a raw pointer read through a Disabled tag (freed by `c.free`)
--> ./s6.lu:12:17
|
9 | let p = c.malloc(8) as *u8
| ----------- the provenance this operation violates was created here
...
12 | let v = p[0]
| ^^^^ the operation that reaches undefined behavior
|
= note: this row licenses O1: `mut` params lower to `noalias` + `dereferenceable`; unique-tag
stores forward without memory checks — compiled code may already have been transformed
under that assumption, so the behavior of an unchecked build is undefined
([mem.prov.state]). The `--checked` machine reports it deterministically instead.
Two implementations, one row, one clause, one span. That agreement is the whole reason to believe either of them.
What a tag is
Both reports talk about tags, and the vocabulary is worth ten minutes because it is the model.
Every pointer value carries a tag: an identity, distinct from the address. Every allocation carries a tree of them. When a pointer is derived from another — a borrow taken, a parameter entered, a door crossed — the new pointer’s tag is a child of the old one’s, and the tree records the family. That is provenance: not where a pointer points, but which lineage it inherits its permission from. Two pointers can hold the same address and have different provenance, and the model treats them as different pointers, because they are.
Each tag has a state per byte of the allocation, and the states are few:
| state | what it means | what ends it |
|---|---|---|
| Reserved | derived, unused for writing | a write through it makes it Active |
| Active | the live writable one | a write through a non-descendant Disables it |
| Frozen | readable, not writable | a write through a non-descendant Disables it |
| Disabled | dead | nothing; a Disabled tag never comes back |
Read the report again with that in hand. tag#0 c.malloc(8)#root Disabled — the allocation’s root tag, killed by the free. read through tag#0 … which is Disabled — the access. Nothing else in the
line is doing any work. The tag tree at the bottom of a report is the
whole story, and learning to read those four lines is learning the
model.
The tree also composes with everything chapter 8 built. Freeing a region Disables every tag of every allocation the region owned, in one motion, the same motion that freed them. Freezing a region moves its tags to Frozen. Tags rooted in different regions never alias, and the compiler knows it without an alias analysis, because region identity partitions provenance. The word for what chapter 8 called a border, one tier down, is a tag.
What you may never do
Wolf’s list of undefined behavior is closed, printed, and eleven rows long. Undefined behavior is the compiler’s license to assume something did not happen: not an error, not a trap, but a fact the code generator is entitled to build on. That is why the list is short and why every row is paired.
The pairing is the deal. A row exists only if some optimization needs it, and the row names the optimization. Adding a row means amending the specification and naming what the amendment buys; removing an optimization means the row goes.
| # | You may never | Because the machine may then |
|---|---|---|
| P1 | use a pointer after its allocation is freed or its permission is gone | forward a store to a later load without re-reading memory; treat mut parameters as unaliased |
| P2 | write through a pointer that is only allowed to read | hoist a read out of a loop across a call it cannot see into; keep immutable data in registers and share it without locks |
| P3 | read or write outside an allocation’s bounds | assume an access of known size is in bounds, and prove two allocations distinct by their extents |
| P4 | touch an allocation whose region was freed | give each region its own alias domain, so pointers into different regions never alias; treat a closed region’s contents as unchanging |
| P5 | assert assume noalias for ranges that overlap | vectorize and reorder the asserted accesses as if disjointness had been proven |
| P6 | lie to a re-entry door about which region the bytes are in | let safe code past the door keep every safe-tier entitlement, re-checking nothing |
| L1 | read memory nothing has written | lower a move to a copy and forget the source; delete stores to moved-from places; skip zeroing locals |
| L2 | dereference a pointer to something that is gone | promote allocations to the stack and to registers without pinning their addresses |
| T1 | build a value a type cannot hold — a bool outside {0, 1}, a discriminant no variant has, a str that is not UTF-8 | pack a niche into unused values, compile a match to a jump table with no default arm, skip re-validating UTF-8 |
| T2 | let another pointer observe a wide write half-finished | reorder fields, split wide stores, and give value fields no address identity |
| C1 | race on non-atomic memory from unsafe or foreign code | move and combine stores across stretches with no synchronization in them |
Eleven entries. Nine of them are this chapter’s; T2 and C1 need two
lines of execution, and Part 3 is where a second one arrives. Every row requires the raw
tier or a foreign call to reach — a program with no unsafe in it
cannot get to any line of this table, which is the sentence the previous
two chapters were building toward.
Notice what is not in the table, because the omissions are load-bearing. Integer overflow is not undefined; it traps, in every profile, and the range facts that come from checking are worth more to the optimizer than the assumption would be. Division by zero traps. An out-of-bounds index on a safe collection traps. A stale handle traps. A leak is defined behavior and always was. Those are the choices chapter 3 and chapter 8 made, and this table is where you can see what they cost and what they bought.
The teaching stance follows from all of it. In C you wonder whether your pointer code is right. In wolf you ask, and the answer is the same answer twice, with a line number.
Exercise 9-5 (fingers + comprehension · lupin) — Inject the classic: write, free, read, through one pointer. Run it twice. What is the oracle’s finding, and — the actual question — what is identical between the two runs that would not be identical for a use-after-free in C?
Exercise 9-6 (comprehension · lupin) — Allocate 64 bytes with
c.malloc and read one of them before anything writes it. No free, no
bounds problem, no aliasing. Predict whether this is undefined behavior,
which row it lands on, and what optimization the row’s license names.
9.4 The one door back
A raw pointer never becomes a safe value by being carried around. It
becomes one at a door, and there are exactly two: borrow r from p, and
a raw index laundered through a pool handle, which revalidates its
generation the way chapter 8 showed. Both are checked crossings. There
is no third.
The first one, used:
import c "stdlib.h"
fn main() -> !int {
var words = 0
let scratch = region()
// # Safety: the allocation is made while `scratch` is the ambient
// region, so it lies inside `scratch`'s footprint; it is freed once.
unsafe {
let p = in scratch { c.malloc(8) as *u8 }
c.memset(p, 9, 8)
let counts = borrow scratch from p
words = counts[0] as int
c.free(p)
}
print("{words}")
0
}
$ lupin door.lu
9
borrow scratch from p is a claim, spelled out: these bytes are inside
that region, alive, and of that type, for as long as the borrow lasts.
What comes back is an ordinary safe value governed by scratch’s rules,
and from there the code is chapter 8’s code again. The claim needs both
halves to mean anything, and the compiler says so when either is
missing:
import c "stdlib.h"
fn main() -> !int {
let scratch = region()
let table = in scratch { List[int]() }
let offset = 8
var words = 0
// # Safety: nothing is discharged here — the door is malformed,
// which is what this program is for.
unsafe {
let counts = borrow scratch from offset
words = counts as int + table.len
}
words
}
error[E1305]: `borrow … from …` needs a raw pointer (`*T`) here, but this is `i32`
--> ./s8.lu:13:42
|
13 | let counts = borrow scratch from offset
| ^^^^^^ the door's claim has nothing to check against
|
= note: door 1 asserts "this pointer addresses that region's live allocation" — it needs the
region on the left and the raw pointer on the right. The other door is a checked
`handle`, which re-validates its generation at every access.
Shapes are the cheap half. The expensive half is whether the claim is true, and no static analysis can settle it — the pointer came from C. So the obligation is discharged at run time, at the door, and a false claim is undefined behavior there rather than at some later read:
import c "stdlib.h"
fn main() -> !int {
var words = 0
let scratch = region()
// # Safety: DELIBERATELY WRONG — the allocation belongs to the
// program's own region, not to `scratch`, so the door's claim is a
// lie and the oracle says so.
unsafe {
let p = c.malloc(8) as *u8
c.memset(p, 9, 8)
let counts = borrow scratch from p
words = counts[0] as int
c.free(p)
}
words
}
$ lupin forged.lu
forged.lu: ub(mem.ub) §7/P6: `borrow region #1 from` a pointer into alloc#0, which is owned by `program` (region #0) — the obligation is that the allocation lies wholly inside the named region's footprint [mem.unsafe.door] at 357..378; tag created at 291..302
licenses O6: safe-tier code after the door keeps all safe-tier entitlements (O1–O4) — the door is where trust concentrates
alloc#0 `c.malloc(8)` 8 byte(s), live, owned by region #0
tag#0 c.malloc(8)#root Active exposed
$ echo $?
3
The two programs differ in one line’s position — where the malloc
happens — and that is the whole difference between a true claim and a
false one.
Read the license on that row, because it is the chapter’s thesis in one clause. After a door, safe code keeps all of its entitlements: the optimizer treats the value that came through exactly as it treats a value that was never near a pointer, hoisting its loads, assuming its exclusivity, re-checking nothing. That is what makes the door worth having, and it is also why lying to it is the worst lie available in the language. Trust concentrates at doors so that it does not have to be spread thin everywhere else.
Exercise 9-7 (comprehension · lupin) — Two programs differ by one
line’s position. Both allocate eight C bytes, both cross back to safe
code through borrow r from p. In the first, the malloc happens
inside in r { }; in the second, outside any window. Predict each
verdict before running either, and state the door’s obligation in one
sentence.
9.5 #include-grade C
import c "stdlib.h" reads like #include because it means what
#include means: the names in that header become available under c.,
with C’s types, calling C’s code. There is no binding layer to generate
and no second language to describe the first one in. What crosses is a
call.
Everything that comes in this way is raw-tier, by decree and without exception. A C function is a black box that takes and returns machine values; nothing about it can be checked, so nothing about it is promised, and no import is ever quietly safe. Every C call in this chapter is inside a ring for that reason alone.
Because the imports are ordinary C calls, they cost what C calls cost. There is no marshalling step, no conversion layer, no wrapper object: arguments go in registers according to the platform’s ABI and the callee is the same code your C programs link against.
Two models and a real one
There is a wrinkle here that is worth a page, because it is the method this whole book is built on, caught doing its job.
Both implementations model the C set. The interpreter has a model of the
heap, and so does the compiler’s checked machine — that is how a
use-after-free through c.malloc produces a report instead of a
segfault. But a model of calloc is not calloc, and the native build
does not model anything: it calls the one in your libc.
In the summer of 2026 a model and the real thing disagreed, and for a
while nobody knew. calloc(n, size) allocates n * size bytes; one
model allocated n. Under it, c.calloc(8, 8) was an eight-byte
allocation, so a program that copied 64 bytes into it was out of
bounds — and the model said so, confidently, with a row number and a
license, about a program that is correct. It surfaced when the same
source was compiled and run against real glibc, which handed back 64
bytes and behaved. That is the differential: one language, more than one
implementation, run against each other, and any disagreement treated as
a bug in one of them until somebody proves which. It was a bug in the
model. The program at the top of
this chapter is the shape that catches it — c.calloc(8, 8) with 64
bytes copied in — and it now runs green three ways.
The book’s claim is not that its tools are correct. It is that when they are wrong, something notices.
The twenty-line wrapper
The pattern for using C from wolf is not a code generator. It is a function:
import c "stdlib.h"
import c "string.h"
fn pack(bytes: uint) -> int {
var written = 0
// # Safety: both allocations own `bytes` bytes for the whole
// block, every C call stays inside them, and each is freed once.
unsafe {
let raw = c.malloc(bytes)
c.memset(raw, 0, bytes)
let out = c.calloc(bytes, 1)
c.memcpy(out, raw, bytes)
c.free(raw)
c.free(out)
written = bytes as int
}
written
}
fn main() -> !int {
print("{pack(64)} bytes out, and not a pointer in sight")
0
}
$ lupin pack.lu
64 bytes out, and not a pointer in sight
$ wolf build pack.lu && ./pack
64 bytes out, and not a pointer in sight
pack’s signature is fully safe — it has to be; §9.2’s E1302 saw to
that. Its body is the only place in the program where a pointer exists.
Everything upstream of it calls a function that takes a number and
returns a number, and no amount of reading the rest of the shelf will
turn up a *T, because there is nowhere for one to be.
This is the shape to copy, and its virtue is arithmetic. A reviewer can
hold twenty lines to the standard “I believe every one of these,” which
is the standard unsafe code actually requires and the standard forty
thousand lines cannot meet. Spread the same C calls across the
application — an unsafe block at each call site — and the audit
surface is the application. The door metaphor closes the argument:
doors work because buildings have few of them.
Exercise 9-8 (fingers · wolf + lupin) — Take pack and change the
c.calloc(bytes, 1) to c.calloc(1, bytes), which allocates the same
number of bytes a different way. Run the program under the interpreter,
then compile it and run the binary. Report both outputs and say what a
difference between them would have meant.
9.6 FFI and regions
C code holds pointers. That is most of what C code does. So the question this section answers is the one every FFI design has to: what may a C function keep, and for how long?
The rule is short. A C call executes against an implicit region borrowed
for the call’s extent. Anything the callee does with a wolf pointer ends
when the call returns. If a C API needs to retain a pointer past the
call — a callback registration, a context struct it stores for later —
then what it is given is a handle, revalidated on the wolf side at
every use, or an allocation inside a region a #[trusted] module pins
for exactly that purpose. Those are the two shapes, and the reason there
are only two is that they are the two that can be checked.
The other direction is where the interesting fault lives. A pointer from C is a plain machine word; it moves out of a region block the way any number does, and nothing stops it:
import c "stdlib.h"
fn main() -> !int {
// # Safety: DELIBERATELY WRONG — the pointer outlives the region
// whose window the allocation was made under.
unsafe {
let p = region command {
let scratch = c.malloc(8) as *u8
c.memset(scratch, 5, 8)
scratch
}
let v = p[0] as int
v
}
}
$ lupin escape.lu
escape.lu: ub(mem.ub) §7/P4: read at alloc#0[0], whose owning region #1 was freed wholesale [mem.prov.region] at 336..340; tag created at 235..246
licenses O3b: one alias-scope domain per region — pointers into distinct regions never alias; O4: regions not open in the current scope yield `invariant.load`
alloc#0 `c.malloc(8)` 8 byte(s), live, owned by region #1
tag#0 c.malloc(8)#root Disabled exposed
$ echo $?
3
Row P4, not P1, and the distinction is the section. Nobody called
free. The report even says the allocation is live — its bytes were
never handed back individually. What died was the region, and the
region’s death took the permissions of everything it owned, wholesale,
in the one motion chapter 8 described as the point of the whole
construct.
So the rule C has to learn on the way in is: memory obtained while a region was open is a loan from that region, and the closing brace calls it in. That is the same sentence chapter 8 wrote about wolf values, applied to a pointer that has no idea what a region is. The pointer does not need to know. The tag knows.
Exercise 9-9 (comprehension · lupin) — A C allocation made while a region was ambient, escaping the region that owned it. The pointer is a plain integer-like value; it moves out fine. Predict what the read faults with, and why the report differs from 9-5’s use-after-free.
9.7 Auditing: #[trusted] and the audit surface
Two of the three rings are the compiler’s business. The third is a person’s.
Some unsafe code maintains an invariant that no checker will ever see:
an allocator’s free-list is well-formed, a pinned buffer outlives every
C callback registered against it, a length field and a pointer agree.
The block’s # Safety: comment states it, and a reviewer either
believes it or does not. #[trusted] is how such a function says so out
loud:
import c "stdlib.h"
import c "string.h"
#[trusted("the scratch buffer never leaves this frame; freed before return")]
fn pack(bytes: uint) -> int {
var written = 0
// # Safety: both allocations own `bytes` bytes for the whole
// block, every C call stays inside them, and each is freed once.
unsafe {
let raw = c.malloc(bytes)
c.memset(raw, 0, bytes)
let out = c.calloc(bytes, 1)
c.memcpy(out, raw, bytes)
c.free(raw)
c.free(out)
written = bytes as int
}
written
}
fn main() -> !int {
print("{pack(64)} bytes out, declared")
0
}
The attribute carries its obligation as a string, and the string is not
decoration: it is the sentence a reviewer checks the body against, and
wolf audit-surface prints it back beside the function’s name.
The other half of the deal is the manifest. A module holding trusted
code is declared in the package’s wolf.pkg:
# wolf.pkg
trusted = root
Forget that line and the audit stops and says so, naming the first trusted function it found and the entry the manifest is missing. That is the entire mechanism, and its modesty is deliberate. The manifest does not make trusted code safe. It makes trusted code countable: a dependency that grows a trusted module has to grow a manifest line to do it, and a manifest line is a diff. What you get is not a guarantee. It is the end of surprise.
Which is worth being plain about, because this is the part of a language
that attracts security theater. wolf audit-surface proves nothing. It
counts rings, prints obligations, and holds the manifest to its own
claims. A reviewer still reads the code. The value is that the reviewer
knows, exactly and cheaply, which code — and that the answer does not
change silently between two releases of a dependency.
The scripts that are not there. Ask a package manager in another language which of your dependencies runs arbitrary code at build time and the honest answer usually takes a script of its own. Wolf has no build scripts at all: a package is source plus a manifest, and there is no hook where a dependency executes anything on your machine before your program does. That is a decision with costs, and chapter 24 is where they get counted. It is mentioned here because it is why this section is short. The audit surface of a wolf dependency is its unsafe rings and its imports, and there is no second surface underneath.
Exercise 9-10 (spelunking · lupin) — pack above wraps its unsafe
block in a #[trusted] function. Run it under the interpreter, then
answer from the chapter: what two questions about this function does a
manifest-and-inventory audit answer that reading the function’s source
cannot?
9.8 The four-tier picture
Part 2 asked one question in four sizes. Here they are on one page, with what each one costs and what happens when it goes wrong.
| tier | the question | the granule | when the target is gone | annotations |
|---|---|---|---|---|
| values | who owns this? | one value | it cannot be — the compiler refuses the program (E1001) | none |
| regions | what dies together? | a group with one death | it cannot be — the compiler refuses the escape (E1010) | one word per group |
shared / handle | who else is holding this? | one object, counted or indexed | shared: it is not gone. handle: a deterministic trap | one word per field |
| raw | what did C hand me? | a range of bytes | undefined behavior, named by row, caught by the oracle | one word per block |
Read down the annotations column. The whole memory architecture of a wolf program is a handful of words placed where a decision was made, and nothing anywhere else. Then read down the column beside it, which matters more: the failure contract weakens exactly one tier at a time, and it only reaches “undefined” in the tier you had to type a keyword to enter.
Here is the shelf, tier by tier, at the end of Part 2. A Doc and its
fields are values, moved and copied by chapter 7’s rules, with no
annotation on any function that merely builds one. A request’s parse
lives in a region that dies at a brace; the store is a region value that
outlives every command; a published snapshot is frozen and readable
forever by anybody. The recency ring is a pool of Doc slots inside the
store, cyclic and cheap, with handles naming entries that eviction may
have taken — and eviction is a trap, not a mystery. And packing a
snapshot for the socket is twenty lines of C calls inside one function,
in one module, listed in one manifest line, holding the only pointer in
the program.
Four tiers, one direction of trust. Every construct either stays in its tier or crosses at a door, and nothing else crosses at all.
Exercise 9-11 (comprehension · prose) — Five fragments; place each on the four-tier map:
let b = copy apool[h].valuep[8] = 1wherep: *u8borrow r from pch.send(move r)whereris a closed region
Exercise 9-12 (design) — A team wraps a 40,000-line C codec behind
wolf FFI. Debate the two candidate shapes: (a) one unsafe block per
call site, spread through the application; (b) one module owning every
unsafe line, exporting twenty safe functions, #[trusted] on the
membrane. Which failure modes does each shape optimize for, and what
does the twenty-line rule from §9.5 actually buy the reviewer?
Part 2, closed
Chapter 7 opened with a question and a warning that the question would keep coming back at different sizes: who owns this, and how big is the granule? It has now been answered four times, and the answers stack rather than compete. One value. One group. One object other people are watching. One range of bytes that came from somewhere else entirely.
What the part cost the reader, counted honestly: mut and take at the
call sites where a mode is a decision, region where a group of
allocations shares a death, freeze where a snapshot is published,
shared/weak/handle on the fields that cross a border, and unsafe
around the code that left the language. There are no lifetimes in that
list, no region parameters, no annotation on the great majority of
functions, which merely allocate and return. The architecture is visible
where it is a decision and invisible everywhere else.
And there is one more thing to notice about the escape hatch before Part 3 starts, because it is the strongest evidence Part 2 has. The unsafe tier is the floor below the model you now trust, and the floor turned out to have simpler rules than the storeys above it — no exclusivity to maintain, no aliasing exam to pass, no borrow checker to satisfy. C’s rules, and a machine that tells you when you broke one. That is not a concession. It is the shape you get when a language decides that the tier with the fewest guarantees should also be the tier with the fewest rules, and puts the cleverness where the compiler is the one doing the work.
Part 3 takes both halves of chapter 8’s closing sentence — a closed region has exactly one owner, and frozen data has no writers — and finds out what they are worth when more than one task is running.
Exercise 9-13 (extension (break-it-on-purpose) · lupin) — Construct
the shortest program you can in which the assertion, not any access,
is the undefined behavior: use assume noalias on two pointers that
alias. Predict the oracle’s wording — what does it say overlaps what?
Exercise 9-14 (comprehension · lupin) — The subtlest report in the
chapter. Take §9.4’s door program and add one line: after
let counts = borrow scratch from p, write p[0] = 1 through the raw
pointer, and only then read counts[0]. Predict where the fault is
reported and what the tag tree at the bottom of the report will have in
it that no other report in this chapter has shown.