rusty_alloc 2.2.0

Allocator core of the rusty_alloc pure-Rust remake of mimalloc v2.4.5: segments, free-list-sharded pages, lock-free cross-thread frees, first-class heaps, arenas and a mi_*-compatible surface. Detects double frees. MIT.
Documentation
# `unsafe` inventory — rusty_alloc

Every `unsafe` block in the workspace carries a `// SAFETY:` invariant, and
that is **lint-enforced**, not conventional: `clippy::undocumented_unsafe_blocks
= deny` and `unsafe_op_in_unsafe_fn = deny` are workspace-level, and clippy
runs `-D warnings` on every PR. This file is the module-level map: what each
module's `unsafe` is *for*, and when it was last deliberately audited.

Census basis: `unsafe` occurrences in CODE per file (comments stripped;
`unsafe fn` signatures and the four `unsafe impl`s included).
**The baseline is now MACHINE-ENFORCED** (H-11): `tools/unsafe-census.sh`
counts `unsafe` in code (comments stripped) per file against
`tools/unsafe-baseline.txt` — **903 occurrences across 23 files, 2026-09-09**
— and FAILS if the total grows. Growth is not forbidden, it is required to be
deliberate: add the new sites here with their purpose and audit date, then
re-run with `--update` in the same commit. `cargo geiger`, the registry's
nominal probe, does not compile on the pinned toolchain in any version tried;
this is the recorded substitution, and a better fit besides — geiger measures
unsafe in DEPENDENCIES, and ours are `libc` plus bindings-only `windows-sys`.

| Module | Count | What the `unsafe` is for | Last audit |
|---|---:|---|---|
| `alloc.rs` | 94 | The public entry points: raw-pointer reads on the malloc fast path (must never form `&mut` on the shared empty-heap sentinel), pointer-derived metadata on the free path (`segment_of`/`page_of`), block-content copies in the realloc family. **+15 on 2026-08-22**: the free path's two `asm!` sites — the memory-destination `used--` whose flags drive the retire branch (its `label` block is a separate item and carries its own `unsafe`), and the fused `cmp {tid}, fs:0` in both `free_inline` and `free_general` — plus `malloc_or`/`malloc_or_slow`, which give `operator new` a fast path whose miss is a tail call. Each asm reads or writes exactly one field it already had a valid pointer to; none widens what the surrounding code could already touch | 2026-08-22 (free campaign; every new block reviewed at the site) |
| `heap.rs` | 71 | Owner-thread page/queue manipulation under raw pointers (no two `&mut Page` may coexist), the aligned-allocation peek, span carving. **+3 on 2026-08-19**: `try_unlink_huge_segment` split out of `remove_huge_segment`. **+15 on 2026-08-22**: `malloc_generic` split into a small entry plus `malloc_generic_walk`, with `grow_front`, `try_guarded` and `drain_delayed` as cold arms — each split adds an `unsafe fn` signature and its block while dereferencing nothing the single function did not — and the immortal `EMPTY_DELAYED` sentinel that lets the heartbeat read its list without a null test. **+2 on 2026-09-07 (P4e, §2.15 of `docs/plans/small-metal.md`)**: the reclamation fixes — one `unsafe` for the periodic `generic_collect` sweep and one for the reclaim-and-retry that runs before `malloc_generic` returns null. Both call `collect_inner`, which allocates nothing and touches only this heap’s own pages on the owner thread; each carries its SAFETY line. `malloc_generic` itself became a safe wrapper over the renamed `malloc_generic_once`, so the split added no signature. **+2 on 2026-09-08:** the medium collect-and-retry ahead of the heartbeat -- one `unsafe` around `page_collect` + `page_pop` on the bin queue front, one reading `free_is_zero` off the page just popped. Both are the SAME operations `malloc_generic_walk` performs on the same page a few lines later, on the owner thread under the heap lock: the block moves earlier, nothing new is dereferenced. Each carries its SAFETY line | 2026-09-08 |
| `init.rs` | 37 | Thread/heap lifecycle: the initial-exec TLS slot (`global_asm!` + fs-relative asm reads), thread-pointer register reads (`fs:0`/`gs:0x30`/`tpidrro_el0`), heap-box creation/teardown, the abandonment path run inside platform TLS destructors. **+1 on 2026-09-09 (`firmware-what-is-left.md` §7.3): an ATTRIBUTE, not an operation** — `unsafe(link_section = ".rodata.…")` on `EMPTY_HEAP_BOX`, bare metal only, so the never-written sentinel lives in flash instead of costing 1,752 bytes of RAM. The token is counted because the census is a text search; the contract it rests on (raw reads only, no page ever stores its address) is the one the `Sync` impl below already carries, and a write would now fault against the flash cache instead of silently landing. `create_heap`'s template copy from it is a `ptr::read` inside the block that already existed | 2026-09-09 |
| `segment.rs` | 35 | Segment/page metadata addressing: the mask trick (`segment_of`), `page_of`'s contract-based indexing — **the bound is now PROVED for every in-segment offset by `proofs.rs` (Kani), not merely asserted** — span tiling, purge/recommit | 2026-08-19 |
| `prim/windows.rs` | 30 | OS FFI: VirtualAlloc family, FLS destructors, QPC, BCryptGenRandom | 2026-08-08 (0.4.0) |
| `page.rs` | 38 | Free-list links written into dead blocks, the lock-free `xthread_free` four-state protocol (loom-modeled in `tests/loom_xthread.rs`), the immortal `EMPTY_PAGE` sentinel — **+1 on 2026-09-09: the same `unsafe(link_section)` attribute as `init.rs`, placing it in flash on bare metal; its free list is permanently null, so nothing writes it**. **+8 on 2026-08-22**: `page_link_local` split out of `page_push_local` so the caller can decrement `used` in one memory-destination RMW, `page_collect_impl` const-generic over whether it also writes the protocol flag, and `USED_OFFSET` — whose value is asserted against `offset_of!(Page, used)` by a unit test, because an asm operand is not type-checked and a field reordering would silently decrement the wrong bytes | 2026-08-22 (free campaign) |
| `prim/unix.rs` | 27 | OS FFI: mmap family, madvise/decommit, pthread keys, /dev/urandom | 2026-08-08 (0.4.0 Darwin decommit fix) |
| `prim/mod.rs` | 18 | The `TlsSlot` abstraction (`unsafe impl Send/Sync`, justified at the impls), dispatch to the platform backends | 2026-08-08 |
| `rusty_alloc_api/src/lib.rs` | 14 | The `GlobalAlloc`/`Allocator` impls forwarding layouts to the core; `unsafe` is inherent to those traits' contracts | 2026-08-19 |
| `os.rs` | 12 | The prim-layer wrapper: commit/decommit/protect plumbing | 2026-08-08 |
| `prim/mock.rs` | 8 | Miri-only mock OS backend (never shipped; `cfg(miri)`) | 2026-08-06 |
| `arena.rs` | 8 | Lock-free chunk bitmap claim/verify, recycled-chunk scrubbing (the 0.1.0-alpha.2 UAF fix lives here: `wait_no_remote_in_flight` on every recycle path) | 2026-08-08 |
| `prim/fixed.rs` | 37 | **New 2026-09-07 (P1 of `docs/plans/small-metal.md`).** The fixed-region backend for a target with no OS: memory is a `&'static mut [u8]` handed over once. **6 of the 18 are `unsafe fn` signatures the prim seam requires** (`alloc`/`free`/`commit`/`decommit`/`reset`/`protect`) whose *bodies contain no unsafe operation at all* — the free list is `AtomicUsize` arrays under a spin lock and the pointers are built with the safe `with_exposed_provenance_mut`, so this backend adds **zero** unsafe dereferences to the shipped crate. The other 12 are in `#[cfg(test)]`: two `&raw mut` static-region handoffs and ten calls through the `unsafe fn` seam, each with its SAFETY line. **+1 on 2026-09-07 (P2):** the region test became two-sided — where the shipped geometry refuses a segment-sized request from a 512 KiB region, the small profile SERVES one, so the test now frees it too. Audited at the site; the module is `allow(dead_code)` and unreachable on every platform that has an arm above it. **+7 on 2026-09-07 (P4b, §2.9/§2.10 of the same plan):** the two-ended `place` rule added ZERO unsafe to shipped code — `place` is a pure arithmetic `fn` and the scan around it is unchanged — and all seven are in `#[cfg(test)]`: one `slice::from_raw_parts_mut` carving the `REGION_ALIGN`-aligned window out of `BACKING` (replacing a `&mut *ptr` that a `repr(align(65536))` static would have needed, which rustc 1.97.1 on MSVC cannot compile), one `ptr::add` to reach that window, and five calls through the `unsafe fn` seam in the placement assertions and in `greedy_segments`, which allocates segments until refusal and frees every one before returning. Each carries its SAFETY line **+1 on 2026-09-09 (morning, `#21`):** the two-sided alignment test frees the SEGMENT_SIZE-aligned page it is served when the region straddles a boundary — `#[cfg(test)]`, banked without a row here at the time; recorded now. **+5 on 2026-09-09 (`docs/plans/finished/region-alignment-bug.md` §7): the module ships its first unsafe OPERATIONS.** (1) `unsafe impl Sync for Region<N>` and (2) the `&mut *self.bytes.get()` in `Region::give` — the once-only handoff the Janus seam has carried since 2.0.0, moved here so the alignment travels with it; a module-wide `REGION_GIVEN` swap precedes the `&mut`, so a second `give` on any instance is refused before it could alias. (3) `unsafe impl Sync for FirstHeapBox`, the bare-metal static holding the first heap's descriptor (handed out once by `take_first_heap_box`, on the one thread such a build has). The other two are `#[cfg(test)]`: a `from_raw_parts_mut` carving the report's misaligned base out of a static for the refusal probe, and a `Box::new_zeroed().assume_init()` allocating a `Region` in place, because materialising a 64 KiB-aligned value on the stack first faults on Windows. Each carries its SAFETY line. **+2 on 2026-09-09 (`docs/plans/finished/region-alignment-dissolve.md`): zero new unsafe in shipped code** — segments now stride from the region's base, and every piece of that is safe: `stride_base` is a relaxed load, `place` takes an `origin` and does arithmetic, `install_region` aligns the base up. The two are `#[cfg(test)]`, the `--cfg ra_aligned_region` arm of the two-sided alignment test (an `alloc` and a `free` through the `unsafe fn` seam: the 2.0.4 straddle case, kept under the knob that restores the mask), and the `Box::new_zeroed().assume_init()` moved under that same cfg — the default arm is a plain `static Region<N>` now, which a 16-byte-aligned type can be on every host toolchain. Each carries its SAFETY line. **+4 on 2026-09-09 (`docs/plans/finished/esp32-large-alloc-ceiling.md`): all four are `#[cfg(test)]`, and shipped code gained none.** The large-allocation ceiling reported from `rusty_zstd` is reproduced against the real extent allocator by `greedy_dedicated`, which serves `huge_alloc`-shaped reservations until the region refuses and frees every one before returning (two calls through the `unsafe fn` seam), plus one `alloc`/`free` pair modelling the whole segment a first small allocation claims. The fix itself -- the `ra_segment_size` geometry knob, `dedicated_segments`, `region_for_allocs` and `region_capacity` -- is arithmetic over the existing atomics and adds no unsafe operation at all. Each carries its SAFETY line | 2026-09-09 |
| `prim/wasm.rs` | 6 | `memory.grow` linear-memory backend | 2026-08-06 |
| `options.rs` | 6 | Env parsing at init, registered-hook invocation | 2026-08-06 |
| `stats.rs` | 3 | Volatile whole-struct snapshot of racy-by-design counters | 2026-08-06 |
| `random.rs` | 2 | OS entropy seeding via the prim layer | 2026-08-08 |
| `lib.rs` | 2 | **New 2026-09-07 (P3).** One `unsafe impl Sync for SingleThreadCell<T>` — the `no_std` half of `ra_thread_local!`. With `std` the macro is `std::thread_local!` verbatim and this type does not exist; without it, a "thread-local" is a plain `static`, sound because the crate serves `no_std` only on single-threaded targets (the same standing assumption as `prim::fixed`: constant thread id, TLS destructors that never fire, a spin lock that never contends). A `no_std` build on a target that grows threads must revisit this type first — the SAFETY comment says so at the impl. **+1 on 2026-09-07 (P5): PROSE, not code.** The census is a text search, and the `compile_error!` that now forces `--cfg ra_single_threaded` on a `no_std` build names `unsafe impl Sync` in its message so the person who hits it knows what they are opting into. Counted, and left counted rather than reworded: the ratchet is allowed to be conservative, and a message that names the thing is worth one line of baseline | 2026-09-07 (P5) |
| `types.rs`, `bins.rs`, `segment_map.rs` | 0 | safe | — |

### The `publish = false` crates

Not published, but not unaudited either — they were covered by nobody until
2026-08-20, when the workspace-root plan's "audited per member" claim turned
out to be true of only 2 of 6 members. All four inherit `[lints] workspace =
true`, so every `unsafe` block in them already carries a lint-enforced SAFETY
comment, and all four are in the H-11 ratchet's baseline.

| Module | Count | What the `unsafe` is for | Last audit |
|---|---:|---|---|
| `rusty_alloc_ffi/src/lib.rs` | 350 | **The workspace's untrusted boundary**: 157 `extern "C"` entry points receiving caller pointers and sizes. Every out-parameter writer null-guards; `mi_posix_memalign` validates alignment (power of two, ≥ `sizeof(void*)`) and returns EINVAL/ENOMEM; all 8 `count × size` sites use `checked_mul`. Panics cannot unwind into C (edition 2024 aborts on `extern "C"` unwind; release is `panic = "abort"`). **+2 on 2026-08-22**: `new_impl` routes through `alloc::malloc_or` so the OOM arm is a tail call rather than a null test that keeps `size` live | 2026-08-22 |
| `rusty_alloc_override/src/lib.rs` | 51 | `malloc`/`free`/`operator new` interposition for `LD_PRELOAD` — thin forwarding to `alloc::*`, plus the `free_inline` export that carries the fast-path body. No state of its own. **+2 on 2026-08-22**: the sized-delete exports follow the same `free_inline` shape as the unsized ones | 2026-08-22 |
| `rusty_alloc_bench/src/*.rs` | 29 | Tier-B kernels and the `.ratrace` replayer. The trace parser's `unwrap`s are infallible by TYPE (`Record::decode` takes `&[u8; RECORD_SIZE]`), and the one genuinely-invalid field returns `InvalidData` | 2026-08-20 (first audit) |
| `rusty_alloc_wasm/src/lib.rs` | 21 | The `ra_selftest` cdylib fixture: raw block writes and read-back checks that prove the wasm build actually allocates | 2026-08-20 (first audit) |

## The `unsafe impl`s (H-21)

| Impl | Where | Justification |
|---|---|---|
| `Sync for EmptyPage` | `page.rs` | never written: `page_pop` returns before its first store when `free` is null, and the sentinel's `free` is permanently null. On bare metal it is placed in `.rodata`, so the claim is enforced by the flash cache as well as by inspection |
| `Sync for EmptyHeapBox` | `init.rs` | never written, never remote-reachable (no page ever stores its delayed-list address; owner_tid 0 matches no thread); readers are raw-pointer reads only. On bare metal it is placed in `.rodata` (2026-09-09), same enforcement |
| `Sync for Region<N>` | `prim/fixed.rs` | the bytes are handed out exactly once: `give` swaps the module-wide `REGION_GIVEN` before forming the `&'static mut`, every later call on any instance is refused without touching the bytes, and nothing else names them (2026-09-09) |
| `Sync for FirstHeapBox` | `prim/fixed.rs` | bare metal only; `take_first_heap_box` swaps a flag and hands the storage out once, on the single thread such a build has; nothing else names the cell (2026-09-09) |
| `Send for TlsSlot` / `Sync for TlsSlot` | `prim/mod.rs` | the slot is an OS TLS key handle; per-thread storage is accessed only by its owner |

## Rules of engagement

- New `unsafe` requires: the `SAFETY:` comment (lint-enforced), a row update
  here, and Miri coverage or an explicit note that the path is
  hardware-gated (the TLS asm reads are `cfg(not(miri))` — their gates are
  `bench/churn.sh` and the corpus sweeps, never Miri alone).
- The `#[must_use]` discipline on lifecycle returns (`adopt_segment`,
  `retire_span`, `remove_segment`, `remove_huge_segment`, `page_push_local`)
  exists because ignoring those returns was the 0.4.0 use-after-free family.
  Do not `let _ =` one without a comment saying why it is terminal — and a
  semgrep rule (`tools/semgrep-rules.yml`) now enforces that in CI. It earned
  its keep on its first run: `remove_huge_segment` still had the old unsound
  shape (a `debug_assert!(false, …)` that vanishes in release, with the caller
  freeing the segment anyway) months after the normal-segment path was fixed.