Skip to content

TRACKING PR OF 0.2.* DEVELOPMENT - #57

Open
williamwutq wants to merge 32 commits into
masterfrom
0.2-backports
Open

TRACKING PR OF 0.2.* DEVELOPMENT#57
williamwutq wants to merge 32 commits into
masterfrom
0.2-backports

Conversation

@williamwutq

Copy link
Copy Markdown
Owner

DO NOT MERGE IT! DO NOT CLOSE IT! This is used for CI purposes

williamwutq and others added 24 commits August 23, 2026 03:12
Backport of #51 to the 0.2.x line. An interrupted non-tail-shrink
`realloc` in `CheckedSlabBStackAllocator` (Rust) /
`checked_slab_bstack_allocator_realloc` (C) could make recovery corrupt
an unrelated live allocation: the shrink committed the block's smaller
count before scrubbing the excess into the free list, so a fault in
between left the excess holding stale payload while the header already
claimed the smaller span. `recover`'s linear scan then read those
orphaned bytes as a valid multi-block in-use marker, strode past a
neighbouring live allocation's header, and reclaimed its interior as
leaked blocks -- writing free-list links over live data.

Invert the order: scrub the excess to a clean zero-overhead free run
(`write_free_run`) before committing the smaller count. The non-atomic
tail shrink keeps its commit-then-discard fast path (safe at the arena
tail). Magic bumped 0.1.1 -> 0.1.2 (patch byte only; the 6-byte compat
prefix is unchanged, so existing 0.1.x files still open).

Unlike the 0.4.x original, the 0.2.x allocator API has no
surviving-handle-on-failure mechanism, so the port keeps only the
on-disk crash-ordering (no `recovered`/`-2` bookkeeping). Correctness
strictly increases: the neighbour-corruption path is gone; the worst
remaining outcome is a leak or an allocation with zeroed tail bytes.

Tests (all green): Rust alloc,set / alloc,set,atomic; C
test-checked-slab / test-checked-slab-atomic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of two first_fit fixes to the 0.2.x line, both surfaced by the
allocator fault-injection fuzz.

#28 (crash-atomic realloc tail-shrink): reclaiming a shrunk tail block
rewrote the block header and footer and discarded the tail as separate
operations, so a fault mid-sequence left header, footer, and physical
size disagreeing -- a state the block-walking recovery cannot repair (it
would truncate the whole block, losing live data). A tail shrink now
narrows only the user-visible length and keeps the block at its physical
size (an oversized block, as a non-tail shrink already does); the tail is
reclaimed on free. Behaviour change: a tail realloc shrink no longer
returns space to the file immediately.

#35 (two recovery bugs):
  * Interrupted tail *grow*: extend zero-fills the payload before the
    header/footer are rewritten, so a crash left a valid block followed
    by a headerless all-zero region that the recovery scan read as a
    size-0 block and rejected -- turning a recoverable crash into a hard
    open failure. Recovery now rolls an all-zero trailing region back by
    truncation (a real block is never all-zero); genuine mid-arena
    corruption still fails loudly.
  * Coalescing free commits the merged size to the header before the
    footer, so a crash left a stale footer that the header-following walk
    missed, later letting a neighbour's coalesce overlap two blocks and
    desync the walk into a hard open failure. Recovery now normalizes
    every block's footer to its authoritative header as it walks.

Both fixes are self-contained recovery/realloc logic with no dependency
on the 0.4.0 surviving-handle API or tail-replace primitives. Added two
targeted recovery tests that construct the corrupted on-disk state
directly (no fault-injection framework); updated realloc_tail_shrink to
assert the new oversized-block behaviour.

Tests (all green): Rust alloc,set / alloc,set,atomic; C test-first-fit /
test-first-fit-atomic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sync

Opt-in compile define that turns plat_durable_sync into a no-op on both
the Windows and POSIX paths. In-process test/fuzz runs tear the store
down logically and reopen it in-process rather than surviving a real
power loss, so skipping the physical sync leaves both the exercised logic
and the on-disk bytes unchanged, while on macOS F_FULLFSYNC otherwise
dominates C test runtime (minutes -> seconds). Inert unless the define is
set; the default build and all production paths still sync. Never enable
for a build that must survive a real crash.

Mirrors the tooling introduced upstream (#39) so the 0.2.x C allocator
test suites can run quickly during backport validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under `atomic` / BSTACK_FEATURE_ATOMIC, GhostTreeBstackAllocator's
in-place tail shrink discarded the freed tail (try_discard) BEFORE
zeroing the retained block's sub-block padding [new_len, aligned_new).
A crash between the two left that padding holding the caller's stale
bytes. A later same-block grow does not re-zero the newly-exposed region
(it trusts the zeroed-memory invariant, ghost_tree.rs:60), so it would
hand those stale bytes back to the caller.

Zero the padding BEFORE discarding the tail, matching the non-atomic
path, which was already correct. A crash now leaves at worst a zeroed
retained block plus an unreclaimed tail (a benign leak ghost_tree already
tolerates), never stale padding. Operation ordering only -- no on-disk
format change, no magic bump.

This is NOT the 0.4.x ghost_tree fix (8d5c9d9 / f226b76): that one fuses
the two steps with the in-sequence tail-replace primitive (Atrunc /
BSTACK_GEN_SPLICE, absent here) and reorders the non-atomic path to
discard-first purely to drive the 0.4.0 surviving-handle-on-failure API
(also absent here) -- porting it verbatim would REGRESS the 0.2.x
non-atomic path. The zeroed-memory invariant violation is the part that
matters without handles, and the zero-before-discard reorder closes it.

Added realloc_tail_shrink_then_grow_reads_zeros as an invariant guard.
Tests (all green): Rust alloc,set / alloc,set,atomic; C test-ghost-tree /
test-ghost-tree-atomic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port of the 0.4.x ghost_tree AVL optimization (#39: 3dbeaef + 8631408
Rust, 90f60ac C) to the 0.2.x line. The AVL internals operate on raw
offsets and were byte-identical to the optimization's base, so the diffs
applied cleanly; no dependency on any 0.4.0 primitive or the three-type
handle API.

alloc/dealloc/realloc of non-tail blocks do less work under the allocator
mutex: the rebalance up-pass no longer re-reads and re-writes each
ancestor through a redundant balance-factor pass (the bf and height from
the node write are threaded into avl_rebalance), and each node now caches
its two child heights in the AVL header's previously-reserved bytes, so
the up-pass and rotations write one node per level and read no children
in the common in-balance case. Rust also swaps the per-op heap Vec path
buffer for a stack array of the fixed MAX_AVL_DEPTH bound. Purely
internal -- ~25-33% lower per-op latency under real F_FULLFSYNC, no API
or observable-behaviour change.

On-disk: magic bumped ALGT\x00\x01\x02\x00 -> ALGT\x00\x01\x03\x00 for
the child-height cache. Existing 0.1.x files stay compatible: only the
first 6 bytes are checked on open, and coalesce_and_rebalance (run every
open) rebuilds the whole tree bottom-up via avl_write_and_update, which
recomputes every node's cache from its children's own maintained height
fields -- so a legacy/zeroed/crash-torn cache is healed on open and never
trusted across a reopen. Added reopen_rebuilds_stale_child_height_cache
to guard that contract.

Tests (all green): Rust alloc,set (25) / alloc,set,atomic (28); C
test-ghost-tree (34) / test-ghost-tree-atomic (37).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…SED_RESULT

Compile-time annotation sweeps ported from the 0.4.x line (Rust: e8d323f,
2d2ec56, 6bc23e2; C: f194a41), adapted to the 0.2.x public API. Attributes
only -- no logic, signatures, or behaviour changed.

- #[must_use] (Rust) / BSTACK_WARN_UNUSED_RESULT (C): public functions
  whose return reports success/failure or hands back a result that should
  not be silently discarded now warn if the caller ignores them.
  Result-returning Rust fns are left alone (Result is already must_use).
  36 Rust annotations; the C macro plus ~84 declaration annotations across
  bstack.h / bstack_alloc.h / bstack_bytevec.h, and two `(void)` casts on
  intentional cleanup-path discards in bstack_bytevec.c.
- #[track_caller]: BStackSlice / BStackByteVec methods with a documented
  panic precondition now report the caller's source location on panic.
- #[inline]: short public functions across the crate, for cross-crate
  inlining.

0.4.x annotations on types absent from 0.2.x (BStackOwnedSlice,
BStackChunk, BStackAllocError/BStackBulkAllocError, the segregated
allocator, and 0.4.x-only BStack ops) were skipped.

Verified: cargo check (default / alloc,set / alloc,set,atomic) clean;
C libbstack-alloc-set{,-atomic}.a compile clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported from the 0.4.x line (Rust cd96b7a, C 6078bdd), adapted to 0.2.x
which has no commit_grow/commit_shrink helpers or fault-injection macro
-- the grow/shrink commit-and-rollback is inlined to match this branch's
extend/discard.

- resize(target): grow (zero-filled) or shrink the payload to exactly
  target bytes; returns the size before the call. Growth follows extend's
  crash-consistency, shrink follows discard's (truncation is the commit
  point). Rejects a shrink below the locked length.
- ensure(target): grow-only, no-op if already >= target; the
  unconditional counterpart of resize.
- ensure_with(target, f) [Rust atomic / C BSTACK_FEATURE_ATOMIC]: grow
  only if shorter, handing the freshly zeroed tail to f for
  initialization before commit. The grown region sits beyond the
  committed length until the final header write, so it is crash-atomic on
  extend's terms without needing a journal.

Tests: Rust tests::resize (6), tests::ensure (4), tests::ensure_with (3,
atomic). C base test 71/71, test-atomic 111/111 (7 resize/ensure + 2
ensure_with added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported from the 0.4.x line (6244267), adapted to 0.2.x: the shared
commit helper lives in lib.rs (no io_core.rs), inlines the grow commit
(no commit_grow/commit_sparse_extend helpers), uses seek+write_all for
scattered writes (no write_at), and drops all fault_point! calls.

- extend_sparse(buf, length) / extend_sparse_batched(writes, length):
  base API. Grow the payload by `length` with a single set_len (OS
  zero-fills the gaps), writing only the supplied buffer(s) into the new
  region. The batched form scatters (relative_offset, data) writes,
  validated as in-range and pairwise non-overlapping; empty data ignored;
  length == 0 is a no-op.
- try_extend_sparse(s, buf, length) / try_extend_sparse_batched(s, writes,
  length): atomic. Add a try_extend-style size guard `s` (apply only if
  the current payload size equals `s`, else Ok(false)/*ok=0; a malformed
  request is still rejected regardless of the size match).

No journal is needed: the whole grown region sits beyond the committed
length, so a crash before the header commit rolls back by truncation,
exactly like extend. C reuses the existing bstack_iovec_t (its typedef
moved into the base section so the base batched API can use it, and its
comment refreshed).

Tests: Rust tests::extend_sparse (11) + tests::try_extend_sparse (7,
atomic). C base test 80/80, test-atomic 127/127 (9 base + 7 atomic
added). New C decls carry BSTACK_WARN_UNUSED_RESULT.

Not ported (out of scope): the process_gen Sparse / BSTACK_GEN_SPARSE
in-sequence variant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ust + C)

`repeat(offset, pattern, count)` overwrites [offset, offset +
count*pattern.len()) with `count` back-to-back copies of `pattern`; an
empty pattern or count == 0 is a no-op. The general form of `zero`. `set`
feature (Rust) / BSTACK_FEATURE_SET (C).

Ported from the 0.4.x line, but WITHOUT its fixed-size write-in-progress
journal (this branch has none): the full count*pattern.len() bytes are
staged in memory and written directly, then durably synced -- slower for
a large region and O(n) memory, but the same result and the same
durability as `set`. The API is now present so the fill-based ergonomic
methods (BStackSlice::fill, BStackByteVec::fill) can build on it.

Tests: Rust tests::repeat (fill/offset/single-byte/noop/past-end-reject/
reopen). C test-set 101/101, test-set-atomic 194/194 (5 repeat tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported from the 0.4.x line (939a089), Rust only (master has no C for
these) and on BStackSlice alone (no BStackOwnedSlice on this branch).
Adapted to this branch's `BStackSlice<'a, A>`: the stack is reached via
the `self.stack()` method rather than master's `self.stack` field.

- Read-only (alloc): get, head/tail, contains, starts_with/ends_with,
  find/rfind, position/rposition, split_at/split_at_mut.
- Write (set): fill (one BStack::repeat call), fill_with, copy_from_slice.
- Atomic compound (set + atomic, each one crash-atomic BStack call):
  copy_from_bstack_slice, copy_within, swap (cross_exchange), reverse,
  rotate_left/rotate_right (process).

#[track_caller] on the methods with a panic precondition (split_at,
split_at_mut, copy_from_slice, copy_from_bstack_slice, copy_within, swap,
rotate_left, rotate_right) and #[must_use] on head/tail, matching master.

Tests: 21 in src/test.rs (alloc_tests) — 20 under set, 29 under
set,atomic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported from the 0.4.x line (03929eb), Rust only (master has no C for
these), adapted to this branch's BStackByteVec<'a, A: BStackSliceAllocator>
over BStackSlice.

- set(index, value): single crash-atomic write; Ok(None) if index >= len
  (get-style convention).
- fill(value): overwrite the populated region via one BStack::repeat
  (no-op on empty). On this line repeat has no journal, so a large fill
  writes the whole region directly.
- reserve_exact(additional): grow to exactly len + additional (no
  amortised over-allocation, unlike reserve).
- shrink_to(min_capacity) / shrink_to_fit(): realloc the backing block
  down to max(len, min_capacity) / len.

The internal capacity helper grow_to was renamed realloc_to and now
handles shrink as well as growth (the allocator realloc already reallocs
in either direction); its two existing call sites were updated. On-disk
header format unchanged.

Tests: 9 added to the existing bytevec test module in vec.rs (set/fill/
reserve_exact/shrink_to/shrink_to_fit + reopen); alloc::vec::tests 40
passed under alloc,set and alloc,set,atomic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported from the 0.4.x line (03929eb), Rust only, adapted to this branch's
BStackByteVec<'a, A: BStackSliceAllocator> over BStackSlice (no
BStackOwnedSlice here). All gated #[cfg(feature = "atomic")] since they
ride BStack::copy / cross_exchange (set + atomic on this branch).

Append-only movers (benign push-style crash model):
  extend_from_within, extend_from_bstack_slice, append_from_owned.
In-place movers (crash-atomic per step, logically torn if interrupted):
  insert, remove, swap_remove, move_tail_into.
copy_into_bstack_slice copies vec bytes out to a same-BStack slice.

append_from_owned/move_tail_into take/return BStackSlice in the positions
master used BStackOwnedSlice. append_from_owned consumes and frees its
argument on EVERY path (foreign-stack, append-error, and success:
`let freed = alloc.dealloc(other); appended.and(freed)`), never leaking.
OOB index/range or u64 overflow -> Ok(None); a cross-BStack handle on a
cross-slice method -> Err(InvalidInput). master doc links to the
nonexistent extend_from_slice were repointed at push.

Tests: 13 added to the inline bytevec test module (happy path + OOB->None
+ cross-BStack misuse + reopen; the foreign-append test also asserts the
rejected slice is reclaimed, proving no leak). alloc::vec::tests 53
passed under alloc,set,atomic; compiles with the methods cfg'd out under
alloc,set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ported verbatim from the 0.4.x line (03929eb) — all the helpers it needs
(read_header, reserve, write_bytes_at, write_len_field) already exist on
this branch. Appends an entire &[u8] in one shot: reserve once, write all
bytes with a single durable set, then commit the new len (vs a
grow/write/len cycle per byte). Empty input is a no-op. Crash-consistent
like the other multi-step methods — a crash before the len commit leaves
the appended bytes beyond the committed length, invisible, and re-running
recovers.

Tests: 3 added (bulk append, empty no-op, persist via raw block).
alloc::vec::tests 43 (alloc,set) / 56 (alloc,set,atomic) passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crate version 0.2.5 -> 0.2.6 (Cargo.toml, Cargo.lock). Following the
per-release convention, the BStack format-version stamp is bumped
BSTK\x00\x01\x0f\x00 (0.1.15) -> BSTK\x00\x01\x10\x00 (0.1.16) in src/lib.rs,
c/bstack.c, c/test_bstack.c, and the README/doc comments. This is
compat-neutral: `open` gates only on the first 6 bytes (BSTK\x00\x01), so
files written by any 0.1.x still open, and 0.2.6 reads older files
unchanged. The on-disk layout itself is identical to 0.2.5.

CHANGELOG: the [Unreleased] section is stamped [0.2.6] - 2026-08-23 and a
fresh empty [Unreleased] opened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backport of the 0.4.x fix. `align_up_len` / `algt_align_up_len` computed
`(len + 31) & !31` unchecked, so any `len > u64::MAX - 31` wrapped and a
near-`u64::MAX` request rounded *down* to a 32-byte block: `alloc` handed
back a handle claiming the requested length, and `realloc` took the shrink
path, physically shrinking the block to 32 bytes and returning the freed
remainder to the tree while the caller's handle still claimed the huge
length. Debug builds trapped on the overflowing add, so the reachable
damage was release-only.

Both are now checked against a new `MAX_ALLOC` / `ALGT_MAX_ALLOC`
(`(u64::MAX - ARENA_START) & !31`, bounded by the arena rather than by
`u64` alone), and `alloc`, `alloc_bulk`, `realloc`, `dealloc` and
`dealloc_bulk` reject an unalignable length with `InvalidInput` / `EINVAL`.
The C helper signals the overflow with `UINT64_MAX`, matching the 0.4.x
port.

Magic bumped `ALGT\x00\x01\x03\x00` → `ALGT\x00\x01\x04\x00` (patch byte
only; existing 0.1.x files stay compatible). The layout docs in
ghost_tree.rs, bstack_alloc.h and README.md still quoted the pre-0.2.6
`\x02\x00` magic and are corrected to the new value.

Rust: 31 ghost_tree tests pass. C: 36/36 (set) and 39/39 (set+atomic),
including the two new rejection tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport of the 0.4.x slice-level wrappers over the CRDS primitives that
have been in the base API since 0.2.3.

cas_on(guard, expected, new_bytes) is one crash-atomic BStack::eq_crds
call: guard's bytes are compared to expected and, on a match, the slice is
overwritten with new_bytes and its prior contents returned — all under one
write lock, so no thread observes the compare and the write as separate
steps. cas_on_ne and cas_on_masked wrap ne_crds and masked_eq_crds the same
way. guard may be any view into the same BStack, including the slice
itself. process(f) is one BStack::process call, exposing the transform
reverse/rotate_left/rotate_right already ride on.

Unlike the 0.4.x original, the three cas_on methods share one
check_cas_args helper rather than repeating the same-BStack and two
length checks inline; the error kinds and messages are unchanged.

The README slice method table was still the pre-0.2.6 list, so the rows for
the ergonomic and atomic-compound methods added in 0.2.6 are filled in
alongside the new ones.

11 new tests; 40 slice tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C counterparts of the slice wrappers added in the previous commit:
bstack_slice_cas_on, cas_on_ne, cas_on_masked and bstack_slice_process,
gated on BSTACK_FEATURE_SET + BSTACK_FEATURE_ATOMIC. Each is one call into
the CRDS primitive that has been in the base API since 0.2.3.

The prior contents come back through an old_buf buffer plus an int *ok
flag rather than an Option, matching the convention bstack_eq_crds and
friends already use. As on the Rust side, the three cas_on functions share
one slice_cas_check helper instead of repeating the same-bstack and two
length checks; errno values are unchanged.

11 tests in test_first_fit.c, which is where the slice-level tests live
(the allocator is just the simplest source of two live slices). C: 27/27
with SET+ATOMIC, 10/10 with SET alone (the new tests compile out), and the
featureless bstack_alloc.c still builds.

Changelog: the C port is folded into the existing unreleased Rust entry
rather than added as a second one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport from the 0.4.x line (the change is on master there, not on the
expensive-slice-access-control branch). The fd (Unix) / handle (Windows)
is equally unique per live instance and, unlike the address, does not
change when the value moves. PartialEq stays pointer identity. Platforms
that are neither Unix nor Windows keep the address hash, which is not
move-stable.

The README trait table said "hashes the instance address" and is updated;
master's copy is stale in the same way.

algos/EQUALITY.md, which the 0.4.x entry cross-references, is not ported —
it documents equality and ordering for the chunk and owned-slice types this
line does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport from the 0.4.x line (master). The atomic realloc grow path staged
a vec![0u8; delta] and appended it with try_extend, writing delta bytes for
a region that is zero anyway. try_extend_zeros applies the identical tail
guard and realises the growth with one set_len on a sparse file, so the
zeroes cost no write I/O and no heap staging. Same guard semantics, same
crash consistency, same zero-filled result; the type docs' crash-
consistency table and thread-safety notes are updated to name the op the
path actually issues.

Item 5 was listed as Rust + C, but the C side needs no change:
linear_vt_realloc grows with bstack_extend and never staged a buffer, on
this line and on master alike.

New test asserts the grown bytes read back as zeros in both builds; 96
allocator tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport from the 0.4.x line (master).

io::Write forwards write(buf) to extend_from_slice and returns buf.len();
flush is a no-op since every extend_from_slice is already durably synced.
Needs only alloc + set.

split_off(at) keeps [0, at) and returns a new vec holding [at, len),
moving the tail straight between the two on-disk blocks with one
crash-atomic BStack::copy — never through process memory, which is why
there is no non-atomic fallback. drain(range) reads the range out, shifts
the tail down with one copy, then commits the shorter len. Both are
in-place movers: crash-atomic per step, torn-but-valid as a whole, matching
insert/remove/swap_remove already in this block. Out-of-range requests
return Ok(None) per the type's convention.

The 0.2 handle type is BStackSlice rather than BStackOwnedSlice, so drain
reads through self.slice directly instead of via as_slice().

The README bytevec section predated 0.2.6 and described none of the atomic
movers; the out-of-bounds convention, the two crash-consistency classes and
io::Write are documented there now.

11 tests; 67 vec tests pass with atomic, 46 without.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport from the 0.4.x line (master). The C bytevec stopped at
push/pop/truncate/reserve/resize/get/read_bytes; everything that mutates
existing bytes or moves them around was Rust-only, including the movers
0.2.6 added on the Rust side.

Adds under BSTACK_FEATURE_SET: bstack_bytevec_set and _fill. Adds under
SET+ATOMIC: extend_from_within, extend_from_bstack_slice,
append_from_owned, insert, remove, swap_remove, copy_into_bstack_slice,
move_tail_into, split_off and drain, all built on bstack_copy /
bstack_cross_exchange — bytevec's first atomic build variant, so the
Makefile gains libbstack-bytevec-set-atomic.a and a test-bytevec-atomic
target running the same suite against it.

The implementations needed no adaptation: every primitive they use
(bstack_copy, bstack_cross_exchange, bstack_repeat, the bstack_slice_*
range calls) has been in this line since 0.2.3/0.2.6, and the C bytevec
already stores a plain bstack_slice_t, which is what master's version
manipulates too. The one master-only piece left behind is bytevec_grow_to's
handling of realloc's -2 "allocation lost" return, which this line's
allocators do not report.

Test file replaced with master's: its harness and the one pre-existing test
were byte-identical, so this is purely additive. 13/13 with SET+ATOMIC,
3/3 with SET alone.

Not ported: bstack_bytevec_extend_from_slice, which does not exist in C on
either line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport from the 0.4.x line (master).

Adds BStackSlice::is_from(allocator) in Rust and the bstack_slice_is_from
macro in C, then uses them: every built-in allocator now checks slice
ownership at the top of realloc, dealloc and dealloc_bulk, before touching
any metadata, and fails with InvalidInput / EINVAL. dealloc_bulk rejects
the whole batch and frees nothing rather than stopping part-way — on this
line linear and first_fit implement it by looping over their own dealloc,
so the batch needed an up-front pass to become all-or-nothing.

Rust: linear, first_fit, ghost_tree, slab, checked_slab, and the
DebugCheckingAllocator wrapper, which needed its own guard since its
handle is a DebugHandle rather than a BStackSlice. Shared ensure_own_slice
/ ensure_own_slices helpers in alloc/mod.rs. C: linear, first_fit,
ghost_tree, slab and checked_slab, via check_own_slice / check_own_slices.

Two deviations from master, both because this line's handles differ:
- No handle is carried back in the error. A BStackSlice is Copy and passed
  by value, so a refused caller still holds it; there is nothing to lose
  and no BStackAllocError to carry it in.
- C realloc does not write the untouched slice to *out on rejection. This
  line's other realloc error paths leave *out alone, and the caller still
  holds the slice it passed.

This was never a soundness issue: slices are (offset, len) coordinates
into a file, not pointers, and reach the payload only through bounds-
checked I/O. The damage would be the receiving allocator recording a free
block it never owned. Correct programs are unaffected.

Docs: a "Foreign slices" section in the alloc module, the reasoning on
is_from itself (the alloc module is private, so its //! docs do not
render), the same under "Foreign slices" in bstack_alloc.h, and a note
plus an is_from row in the README slice section.

Rust: 2 new tests, and every allocator module passes (99 alloc, 51
first_fit, 31 ghost_tree, 17 slab, 30 checked_slab, 36 debug_checking, 67
vec). C: 1 new test; all five suites pass in both SET and SET+ATOMIC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All five workflows gated their push and pull_request triggers on master
alone, so nothing on this branch has been built or tested by CI. Adds
0.2-backports to both trigger lists in cci, ci, check, alloc_fuzz and
devskim. The branch name is quoted so it cannot be read as a numeric
scalar.

Adding it to pull_request as well covers PRs targeting this branch; a PR
from here into master already matched, since that filter is on the target.

Also adds the test-bytevec-atomic step to CCI, which the previous commit's
Makefile target introduced but nothing ran. It is skipped on Windows like
the other atomic targets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread c/bstack.c Dismissed
Comment thread c/bstack.c Dismissed
williamwutq and others added 5 commits September 2, 2026 09:48
drain, the only user of std::ops::Range in vec.rs, is behind
#[cfg(feature = "atomic")], so a set+alloc build without atomic saw the
import as unused — an error under CI's -D warnings.

Clippy now passes with -D warnings across all seven feature combinations
CI checks: all-features, no-default-features, set, atomic, "set atomic",
alloc and "set alloc".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Crate version 0.2.6 -> 0.2.7 (Cargo.toml, Cargo.lock). Following the
per-release convention, the BStack format-version stamp is bumped
BSTK\x00\x01\x10\x00 (0.1.16) -> BSTK\x00\x01\x11\x00 (0.1.17) in src/lib.rs,
c/bstack.c, c/test_bstack.c, and the README/doc comments. This is
compat-neutral: `open` gates only on the first 6 bytes (BSTK\x00\x01), so
files written by any 0.1.x still open, and 0.2.7 reads older files
unchanged. The on-disk layout itself is identical to 0.2.6.

The allocator magics need no bump here: GhostTree was already bumped this
cycle (ALGT 0.1.3 -> 0.1.4) for the align_up_len fix, and nothing else
changed what an allocator writes — the foreign-slice guard rejects a call
that was always a caller error and leaves correct programs byte-identical.

CHANGELOG: the [Unreleased] section is stamped [0.2.7] - 2026-09-02 and a
fresh empty [Unreleased] opened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `inline` to the 33 file-local helpers master already marks
`static inline` — the platform shims and header writers in bstack.c, the
free-list/overhead accessors and small arithmetic helpers across the slab,
checked-slab and ghost-tree allocators, and the bytevec header
readers/writers and LE codecs. Determined by intersecting master's
`static inline` set with this line's plain-`static` set, so nothing outside
that overlap changed. 39 sites in all: a few helpers have a second
definition under a different feature guard.

No behaviour change — internal linkage either way, and the compiler was
already free to inline these. Compiles clean (-Wall -Wextra -Wpedantic) for
each of bstack.c, bstack_alloc.c and bstack_bytevec.c across no-features,
SET, and SET+ATOMIC, and the test programs still link.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport of 72abbb5 and ddb8b7b from master, restricted to the types this
branch has: `BStackRange`, `BStackOwnedSlice` and `BStackChunk` do not exist
here, so `Display` lands on `BStackSlice` (`start..end`), `BStackReader`
(`@position`), and `BStackSliceReader`/`BStackSliceWriter`
(`start..end@cursor`).

`BStackReader` was the only cursor type without `Debug` — the slice reader and
writer both had one — so a struct holding one could not derive `Debug`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
williamwutq and others added 3 commits September 2, 2026 16:23
`cargo clippy --all-features --lib --tests` failed with one denied
`reversed_empty_ranges` error and 80 warnings; it is now clean, as are the
no-default-features, `alloc`, and `alloc,set` configurations.

Mechanical fixes applied by `cargo clippy --fix`: 46 needless borrows, an
unnecessary `to_vec`, a `clone` on a `Copy` handle, and a length comparison
to zero.

Fixed by hand:

* The 20 lifetime-extending transmutes in `test.rs` had an inferred target
  type (`transmute::<&mut [u8], _>`), which can silently reinterpret the
  pointee; each now names both types.
* `with_state`'s `#[must_use]` result is bound with `let _` in the three
  `should_panic` tests that call it only for the panic.
* Scoped `#[allow]`s where the lint is wrong about intent: one-element range
  arrays are the shape `get_batched`/`with_state` take, and
  `get_batched_end_less_than_start_returns_error` passes a reversed range
  precisely because that is what it asserts on.
* Renamed the inner `mod alloc_fuzz_tests` to `mod tests`, matching the rest
  of the crate and dropping the module-inception warning.

No behavioural change; lint-only, so no changelog entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ter`

Backport of 5a168c0 from master. The byte-vec iterator implemented only
`Iterator`; it now also has `Clone`, `DoubleEndedIterator`,
`ExactSizeIterator` and `FusedIterator`.

`Clone` forks an iteration at its current position. `next_back` shrinks the
snapshotted `len` and reads there, so `.rev()` and `.last()` cost one read
rather than a full scan. The `len` snapshot taken at construction is what
makes the iterator soundly fused. `size_hint` was already exact, including
its `usize::MAX` clamp, and is untouched.

Master's version of this commit justified the additions as parity with
`BStackChunkIter`, which does not exist on this branch; the traits stand on
their own merits here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backport of 66204ec from master, covering the five allocators this branch
has (master additionally has `SegregatedBStackAllocator`).

Each carries a `PhantomData<Cell<()>>` marker to remove `Sync` where the
allocator is not thread-shareable. `Cell` removes `RefUnwindSafe` along with
it, which nobody chose: `catch_unwind` over an `&allocator` compiled with
`atomic` and not without. An explicit impl restores it — the only interior
mutability is the `BStack`'s own poisoning lock, which is itself
`RefUnwindSafe` via poisoning.

`Sync` is still absent without `atomic`; verified with a throwaway probe that
the five allocators gain `RefUnwindSafe` and that `Sync` still fails to
resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants