Open and answer this issue on GitHub.
ready · opened 2026-10-24 · updated 2026-10-24 · 0 comments
Every unsafe the toolchain needs lives in a crate of its own under crates/unsafe/ (ADR-218): byte-blocks, c-boundary, c-text, checked-text, child-usage, crc32c, file-ring, low-bits, polled, proven-index, ref-or-box, shared-bytes, tether, text-key. Each one is its own workspace, and crates/nikaia-std forbids unsafe_code. So these crates are the only place where memory safety depends on hand-written reasoning instead of the compiler. Each crate’s README states that reasoning for every unsafe.
The owner’s goal is reliability that people outside the project can see. A check that runs only on someone’s machine does not count. It has to run in CI, regularly, and it has to be documented where readers look. The owner asked for Miri, and for fuzzing and Kani where they make sense and are possible, all built into the existing workflow rather than a second one.
unsafe_crates job of .github/workflows/nikaia.yml runs scripts/check-unsafe-crates.sh on every crate:
fmt --check, test and clippy -D warnings, with and without --all-features;cargo +nightly miri test --all-features under Stacked Borrows and again under Tree Borrows (nightly is installed in the job; ADR-001 D1’s one build toolchain stands, because Miri only checks the crates’ tests).scripts/ci-changes.sh sets unsafe: on a change to crates/unsafe/ or to the script, on workflow_dispatch, and on the weekly schedule (17 3 * * 1, which runs the whole workflow on main). On a push to develop it gates promote.fuzz/ directory, and cargo-fuzz is not installed anywhere in CI.#[cfg(kani)] harness.unsafe code passes.unsafe code is checked, or how.nikaia.yml, next to the existing job. There is no second workflow.unsafe_crates stays as it is (Miri and the stable checks). Beside it come unsafe_kani and unsafe_fuzz, under the same condition (needs.changes.outputs.unsafe). All three gate promote on a push, as unsafe_crates does today.41 2 * * *). ci-changes.sh sees which cron fired (github.event.schedule) and turns on only unsafe for the daily one. The weekly one keeps turning on everything. So drift in nightly Miri or in Kani shows within a day, and the daily run costs only the three unsafe jobs.promote has to mean the same on every run. On a push, each target runs a fixed number of runs (-runs=) with a fixed seed and no cached corpus. The daily run uses a larger budget, keeps the corpus between runs with actions/cache, and its findings do not wait for the next push. The budget is a number of runs, never a time.-Zmiri-many-seeds=0..8 for the crates with threads or atomics (shared-bytes, tether). polled and file-ring stay outside Miri, as ADR-218 states (epoll and io_uring); neither fuzzing nor Kani can reach them either.unsafe rests on arithmetic or a bit layout that a bounded check can cover completely. Not where it rests on the kernel or on C (c-boundary, child-usage, polled, file-ring), and not on the SIMD paths, which Kani’s models of the intrinsics do not cover reliably:
proven-index: under the stated precondition, the unchecked index or arithmetic gives the same result as the checked one and is never UB. One harness per operation and integer width.low-bits: the bzhi path equals x & ((1 << n) - 1) for every x and every n. If Kani does not model bzhi, check the portable path and keep the equality as a fuzz target.ref-or-box: building one and reading it back returns the same variant and the same address, for both sides. Option<RefOrBox<A, B>> is one word.checked-text: for every byte string up to a bound (e.g. 8 bytes), it accepts exactly when std::str::from_utf8 does, and the text read back equals the input.text-key: the bytes read back are the key’s own, for each key type the crate knows.cargo-fuzz (libFuzzer, nightly), compared with a safe reference wherever one exists:
checked-text: arbitrary bytes, compared with std::str::from_utf8;c-text: arbitrary bytes with a zero byte appended; the length and the bytes read back must equal the input up to the first zero;byte-blocks: arbitrary 64-byte blocks and sets, the vector path against a scalar loop;crc32c: arbitrary words, the crc32 path against the table-driven path;low-bits: arbitrary (x, n), against the mask;shared-bytes and tether: an arbitrary sequence of operations (make, clone, slice, drop), checked against a plain Vec<u8> model.cargo deny. They can get their own issue if someone wants them.41 2 * * * beside 17 3 * * 1 in nikaia.yml;github.event.schedule to ci-changes.sh: the daily cron sets only unsafe=true, the weekly one keeps all;rustc +nightly -V in the log of unsafe_crates, so a drift can be dated.workflow_dispatch still turns everything on. Check the cron branch of ci-changes.sh locally by setting the variable by hand.check-unsafe-crates.sh, add -Zmiri-many-seeds=0..8 for shared-bytes and tether, under both aliasing models.
unsafe_crates.#[cfg(kani)] mod proofs in each crate named above;cfg(kani) in each crate’s [lints.rust] unexpected_cfgs, so clippy stays clean;--kani to check-unsafe-crates.sh, so a contributor runs it the same way as CI;unsafe_kani to nikaia.yml, with the Kani version pinned (cargo install --locked kani-verifier@<v>, cargo kani setup), and add it to promote’s needs;crates/unsafe/<crate>/fuzz/, a crate of its own outside the crate’s workspace, so the workspace and its Cargo.lock stay as they are;--fuzz RUNS to check-unsafe-crates.sh;unsafe_fuzz to nikaia.yml and to promote’s needs. On a push it runs -runs=100000 -seed=1 with no cache. On the daily cron it runs -runs=1000000 with the corpus cached per target. A crash uploads the reproducing input as an artifact.issues: write, if: github.event_name == 'schedule'). On a failure in any job, it opens “unsafe crates: scheduled check failing” (label ready), or comments on it if it is already open, with the run link, the failing job and the nightly version. On a green run, it closes that issue.
workflow_dispatch input that forces a failure opens the issue, and the next green run closes it.guide/ (and on the website, where the guide is published), “How the unsafe code is checked”, with the per-crate table from the decision above and a link to the workflow’s runs;README.md;scripts/ci-changes.sh, say what the daily cron turns on.nikaia-std, the guide and a note in an ADR that decides nothing new for the language.