Skip to content

TRACKING PR OF 0.2.* DEVELOPMENT - #57

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

TRACKING PR OF 0.2.* DEVELOPMENT#57
williamwutq wants to merge 15 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 15 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>
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.

1 participant