unsafe { ... } authorizes calls and operations marked unsafe. It is an
expression, so its tail value can be bound or returned:
fn main(): void {
size := intrinsics.size_of[int]()
align := intrinsics.align_of[int]()
value := unsafe {
block := intrinsics.alloc(size, align)
slot := intrinsics.ptr_cast[u8, int](block)
intrinsics.store[int](slot, 42)
read := intrinsics.load[int](slot)
intrinsics.dealloc(block, size, align)
read
}
println("value ${value}")
}Without the block, each of those calls is a hard error:
fn main(): void {
size := intrinsics.size_of[int]()
align := intrinsics.align_of[int]()
block := intrinsics.alloc(size, align)
println("${size} ${align}")
}ATOLL4010: call to unsafe fn alloc requires an unsafe { ... } block or an enclosing unsafe fn. Note that size_of and align_of are safe — the
permission you need is exactly as narrow as the operations you call.
What it changes
Unsafe code may perform operations whose initialization, lifetime, alignment, aliasing, or representation requirements cannot be verified normally. It does not disable parsing, name resolution, generic checking, or type checking, and it does not:
- make an invalid cast correct;
- keep an allocation alive;
- pin a movable buffer;
- turn a raw pointer into an owned value;
- let a raw pointer cross a suspension point;
- guarantee that a host-provided address is inside guest memory.
The permission is lexical. Calling an unsafe operation needs an enclosing unsafe block even when the surrounding function is mostly low-level work, which keeps the proof boundary visible during review.
Pointer kinds
ptr[T] is an arena-relative pointer used by managed runtime code. rawptr[T]
is an absolute WebAssembly linear-memory address for constrained low-level
regions, produced by intrinsics.to_raw.
Raw pointers cannot stay live across suspension and cannot be stored in arena-relative managed storage. The compiler diagnoses known escapes; the unsafe block still owns:
- valid allocation and alignment;
- initialized reads;
- correct element type and size;
- no use after release or reallocation;
- compatible aliasing and mutation;
- balanced ownership when bypassing managed wrappers.
Pointer arithmetic is layout-sensitive. Ask the compiler rather than repeating a constant:
struct Row { id: u32, score: u64 }
fn main(): void {
stride := intrinsics.size_of[Row]()
align := intrinsics.align_of[Row]()
second := unsafe {
block := intrinsics.alloc(stride * 2, align)
first := intrinsics.ptr_cast[u8, Row](block)
intrinsics.store[Row](first, Row { id: 1, score: 10 })
raw := intrinsics.raw_add(intrinsics.to_raw[u8](block), stride)
next := intrinsics.raw_cast[u8, Row](raw)
intrinsics.raw_store[Row](next, Row { id: 2, score: 20 })
read := intrinsics.raw_load[Row](next)
intrinsics.dealloc(block, stride * 2, align)
read
}
println("row ${second.id} scored ${second.score}")
}Suspension
An absolute address can become invalid when execution yields and the runtime
changes arena state, so the compiler rejects a raw pointer that is live across a
suspension point. This is checked while lowering, which means a program can pass
atoll check and still fail atoll build:
fn main(): void {
len := 8usize
align := intrinsics.align_of[u32]()
unsafe {
block := intrinsics.alloc(len, align)
raw := intrinsics.to_raw[u8](block)
println("progress")
intrinsics.mem_fill(raw, 0u8, len)
intrinsics.dealloc(block, len, align)
}
}ATOLL4011: rawptr value is live across a suspend point. The fix is structural:
finish the raw work inside a non-suspending region and hand back a managed
value.
fn decode_word(view: [..]byte): int? {
if view.byte_len() < 4 {
return None
}
value := unsafe {
// SAFETY: the length check above covers all four bytes, and the region
// does not suspend between `byte_ptr` and the load.
pointer := intrinsics.ptr_cast[u8, u32](view.byte_ptr())
intrinsics.load[u32](pointer)
}
return Some(value.to_int())
}
fn main(): void {
println("abcd -> ${decode_word("abcd".bytes()) ?? -1}")
println("ab -> ${decode_word("ab".bytes()) ?? -1}")
}Do not call a suspending file, network, task, stream, or clock operation while a raw pointer is part of the live computation.
Containment
Keep unsafe blocks small and put a total, safe signature around them. The check that justifies the block belongs at the boundary, in front of the caller, not buried inside the implementation:
struct Header { kind: u32, length: u32 }
fn read_header(view: [..]byte): Header? {
size := intrinsics.size_of[Header]().to_int()
if view.byte_len() < size {
return None
}
header := unsafe {
// SAFETY: the view holds at least `size_of[Header]` bytes, the wire
// layout is fixed by the protocol, and nothing here suspends.
pointer := intrinsics.ptr_cast[u8, Header](view.byte_ptr())
intrinsics.load[Header](pointer)
}
return Some(header)
}
fn describe(view: [..]byte): string {
match read_header(view) {
Some(header) => "kind ${header.kind} length ${header.length}"
None => "truncated"
}
}
fn main(): void {
println(describe("abcdefgh".bytes()))
println(describe("ab".bytes()))
}The unsafe region is three lines wide, the precondition is a length test the
caller cannot skip, and the failure mode is None rather than a corrupt value.
Prefer an existing standard-library wrapper over writing your own, because it
already participates in suspension, cleanup, and host ABI rules.
Review checklist
For each unsafe block, a reviewer should be able to answer:
- Where did every pointer originate?
- What proves the range and alignment?
- Which value owns the allocation for the whole access?
- Are reads initialized and stores valid for
T? - Can mutation reallocate or alias the same storage?
- Can control flow suspend, return, or raise an error before cleanup?
If the proof cannot be stated locally, move the operation behind a smaller, well-tested wrapper.