rusty_alloc 2.0.5

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
[package]
name = "rusty_alloc"
description = "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 = "https://docs.rs/rusty_alloc"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
readme = "README.md"

# The crate has NO dependencies on any target it currently ships to — this
# entry is compiled ONLY for targets that lack a 64-bit atomic (32-bit RISC-V
# and Xtensa: the Janus ESP32-C3/C6/S3 parts), so the x86-64, aarch64, wasm32
# and Windows builds remain dependency-free.
#
# Why a shim here and NARROWING everywhere else (P3 of docs/plans/small-metal.md):
# the crate's other 64-bit atomics are BITMAPS, whose word width is a free
# choice, and those were narrowed to `u32` — no dependency, still lock-free.
# These two are different: `options::{get,set}` are `i64` in an API frozen at
# v2.0.0, and `DeferredFreeFun`'s heartbeat is a `u64` in the C ABI shared with
# mimalloc's `mi_deferred_free_fun`. Narrowing either breaks a contract, and
# hand-rolling a 64-bit atomic out of two 32-bit halves inside an allocator is
# exactly the kind of thing that produces a subtle bug.
#
# `optional`, and enabled by `std` rather than by the target alone. The use site
# is gated `all(not(target_has_atomic = "64"), feature = "std")` -- a `no_std`
# build uses `options::split64` instead -- so a firmware was fetching and
# compiling a crate it then discarded at link time. Zero bytes on the device
# either way (verified: after `split64` landed, `portable_atomic` symbols in a
# shipped ESP32 image went to zero and its 4,288-byte `LOCKS` table left the
# symbol map), but an unused crate still shows up in an SBOM, a `cargo audit`
# and every dependency count a firmware reviewer looks at.
[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
portable-atomic = { version = "1", default-features = false, features = ["fallback"], optional = true }

[features]
# `std` is DEFAULT and additive — unlike the geometry `--cfg` (P2), which is
# deliberately not a feature. A feature is right here precisely because it IS
# additive: if any consumer in a graph needs `std`, enabling it for all of them
# is harmless. Turning it OFF is what a firmware does, and it selects the
# single-heap profile: no `thread_local!`, no environment, no `process::abort`
# (see `lib.rs`). P3 of docs/plans/small-metal.md.
default = ["std"]
# Pulls `portable-atomic` ONLY where it is actually reachable: a target without
# 64-bit atomics that also has `std`. On every other target the dependency is
# not in the graph at all -- the `dep:` reference resolves against a
# target-gated declaration, so it simply does not apply where the target section
# does not match.
std = ["dep:portable-atomic"]
# Full invariant checking: list walks, canaries, double-free detection (our `dmi`).
debug_checks = []
# Feature-gated rdtsc path profiler (§7.5 of the plan). OFF = byte-identical build.
profile = []
# EXPERIMENTAL (2026-08-20): the free-list link BOUND without the encoding.
#
# `secure` costs a flat ~15 instructions per allocation, and almost all of that
# is the per-page keyed encode/decode plus key generation — not the bound
# itself, which is two ALU ops on a value already in a register. This feature
# isolates the bound so its cost can be measured on its own.
#
# What it buys WITHOUT encoding: an attacker who overwrites a plain link can
# still choose the value, but the bound confines the target to the SAME 32 MiB
# segment — which removes GOT entries, vtables, stack addresses and saved
# return addresses from reach entirely. Strictly weaker than `secure` (which
# also denies steering at all without both per-page keys), strictly stronger
# than today's default, which follows any value it is handed.
#
# Not on by default and not yet recommended: see the plan file for the
# measurement and the false-positive testing before promoting it.
linkcheck = []
# EXPERIMENTAL (2026-08-20): per-page block-liveness bitmap.
#
# The one defence that actually stops the residual in R-005. Encoding and
# bounds both try to prevent a link being FORGED; this detects the thing a
# forgery is for — `malloc` handing out a block that is already live. It does
# not care how the link was forged, so it covers key recovery, relative
# forgery and blind luck alike.
#
# The bitmap is out-of-band with respect to the free list itself and carries no
# key, so unlike the encoding it is not defeated by a read primitive.
#
# OFF by default and independently switchable from `secure`/`linkcheck` so it
# can be measured, and reverted, on its own. Composes with either.
blockmap = []
# Hardened build (our `smi`). M8. Deliberately NOT default — see the cost below.
#
# Always on with this feature: per-page encrypted free-list links, plus a
# same-segment + alignment bound on every decoded link (`page::link_is_plausible`).
#
# Available but INERT until asked for: guarded objects and their guard pages.
# The `guarded_max` OPTION defaults to 0, so `init::create_heap` skips both
# `guarded_set_*` calls and the heap keeps `guarded_rate: 0`. Enabling `secure`
# alone therefore does NOT give you guard pages — set the `guarded_max` option
# > 0, or call `guarded_set_sample_rate` / `mi_heap_guarded_set_sample_rate`.
# And only where a page CAN be protected: `prim::fixed` (bare metal) and
# `prim::wasm` have no MMU, so there the sampler is compiled out and the
# setters leave it off rather than hand out unprotected "guarded" segments.
#
# Measured cost (2026-08-20, callgrind Ir, same binary both arms): a FLAT ~15
# instructions per allocation — 8-25% per-op depending only on how cheap the op
# is (batch 60->75, big 171->185, `usable` and `huge` exactly +0.00 since
# neither walks a free list) and +0.6-1.8% whole-program on lua/perl/sqlite.
# Opt-in because that is enough to forfeit mimalloc parity, which is the
# headline result: perl 0.9991 -> 1.0160, sqlite 1.0003 -> 1.0061,
# calloc 0.949 -> 1.041, batch_lifo 1.008 -> 1.259.
secure = []

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60", features = [
    "Win32_Foundation",
    "Win32_System_Memory",
    "Win32_System_SystemInformation",
    "Win32_System_Threading",
    "Win32_System_Performance",
    "Win32_System_ProcessStatus",
    "Win32_Security_Cryptography",
] }

[target.'cfg(unix)'.dependencies]
libc = { version = "0.2", default-features = false }

[dev-dependencies]
loom = "0.7"
# H-28: the documented invariants get PROPERTIES, not just examples. Kept
# dev-only and vet-exempted at safe-to-run — it can never reach a published
# artifact.
proptest = { version = "1", default-features = false, features = ["std"] }

[lints]
workspace = true