unsafe Topic Is a Small Crate of Its OwnStatus: Accepted
Date: September 26, 2026
Answers: the owner’s decision, weighing where the one unsafe line a
struct of views needs should live: not in nikaia-std — one small extra crate
for every unsafe topic; they stay in the repository but have a reason to
exist of their own, can be used by others, can be audited, and can have their
own tests and toolchain
Related: ADR-001 D1 (one toolchain for building, which
stands), ADR-283 (the tether, the first topic),
ADR-002 D1 (a dependency is kept to what it is worth),
ADR-306 (proven-index, an index read without its check),
ADR-302 D3 (a call into foreign code is written inside unsafe)
unsafe was spread over ten files of nikaia-std, and the emitted code wrote
it too (unsafe { nikaia_std::tether::hold(…) }). Nothing checked any of it
beyond the ordinary tests. The first check that could — Miri, on the code
moved out for this record — found two cases of undefined behaviour in code
the compiler already relied on:
Keep::put handed out a reference into a Box and then moved the Box,
which invalidates the reference;Held, and the __Tethered struct the emitter wrote for a task’s tethered
binding, stored a stretched &'static str as a plain field, which promises
the compiler the text is valid for any call the value is passed into — and
the value may drop the last handle on its buffer inside that call.Both were fixed in the same package, and are why this record is more than housekeeping.
unsafe topic, one crate, under crates/unsafe/<name>.A topic is one thing the language needs that safe Rust cannot say: a view
held beside its buffer, a one-word error, a mapped file, the I/O ring, the C
boundary, an index proved inside. The crate has a name that says what it does and not that it is
Nikaia’s, a README.md that lists every unsafe in it with the argument
for it, no dependency it does not need, and an API the compiler’s generated
code can use without writing unsafe itself wherever that can be arranged.
Its own Cargo.lock, excluded from Nikaia’s workspace and reached from it as a
path dependency. scripts/check-unsafe-crates.sh runs, per crate: cargo fmt
--check, the tests and clippy -D warnings on stable, and Miri on nightly
under both of its aliasing models (Stacked Borrows and Tree Borrows). CI runs
the script as a job of its own.
Nightly is used for checking these crates’ tests and nothing else. What is
built — the compiler, nikaia-std, the crates as dependencies, the generated
programs — stays on stable (ADR-001 D1), and a crate here that
needed a nightly feature would fail Nikaia’s own stable build.
tether.crates/unsafe/tether holds Keep and Held, Holding<V> - a struct of
views beside the one buffer it points into, with a safe constructor (the
struct is built from the buffer inside a closure that must work for every
lifetime, and Views makes rustc check covariance) - Rebase, which builds
a Holding from the views a value was made of, and Dangling<T>, the storage a
value with stretched views needs. nikaia_std::tether re-exports it, and the
emitted code names it by that path. It has no dependencies; self_cell was measured beside
it and costs one allocation per value (~13 % in a container loop), which this
does not.
The wakers, a topic that needed no crate at all: std::task::Wake has built a
Waker from an Arc safely since Rust 1.51, so the wakers are built on it
and no RawWaker vtable is written by hand.
nikaia-std without unsafe.The owner’s follow-up: the remaining unsafe goes out of nikaia-std too.
| crate | topic | from |
|---|---|---|
ref-or-box |
one word that is a &'static A or an owned Box<B>, with a niche |
error’s tail |
checked-text |
bytes checked as UTF-8 once, chunked across rayon (feature), a file mapped and checked (feature) |
fs |
c-text |
text a C library owns; an extern declaration returns Option<CText>, so the promise is made where C is declared |
foreign |
polled |
a poller that owns every descriptor it watches | rt::readiness |
file-ring |
io_uring file operations with owned buffers, and an eventfd bell rung by safe std writes |
rt::uring |
proven-index |
an element read or written without its bounds check, at an index the compiler proved inside (ADR-306) | new, not moved |
libc is not a dependency of nikaia-std (a test’s mkfifo is the command).
nikaia-std says #![forbid(unsafe_code)], so the compiler itself holds
that no unsafe is left in it.
Where Miri cannot run a crate’s tests — polled (no epoll) and file-ring
(no io_uring) — the README says so, and says what upholds the argument
instead: the unsafe there is a contract with the operating system, not a
question about memory.
The emitted code. It writes no unsafe for the tether: a value a task
takes with it is a Holding over the task’s keep, built by Rebase from the
views it was made of. It writes unsafe only around a call into foreign code
(ADR-302 D3) and around proven-index’s read or write at an index
proved inside (ADR-306) - the one operation each proof licenses.