Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/cci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,21 @@ jobs:
run: cd c && make test-set-atomic
- name: Build and test first-fit allocator
run: cd c && make test-first-fit
- name: Build and test first-fit allocator (atomic)
if: runner.os != 'Windows'
run: cd c && make test-first-fit-atomic
- name: Build and test ghost-tree allocator
run: cd c && make test-ghost-tree
- name: Build and test slab allocator
run: cd c && make test-slab
- name: Build and test slab allocator (atomic)
if: runner.os != 'Windows'
run: cd c && make test-slab-atomic
- name: Build and test checked-slab allocator
run: cd c && make test-checked-slab
- name: Build and test checked-slab allocator (atomic)
if: runner.os != 'Windows'
run: cd c && make test-checked-slab-atomic
- name: Build and test bytevec
run: cd c && make test-bytevec
- name: Build alloc libraries (all feature variants)
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ jobs:
- name: Test with set and atomic
run: cargo test --features "set atomic"
- name: Test Release with set and atomic
run: cargo test --release --features "set atomic"
run: cargo test --release --features "set atomic"
- name: Test with alloc, set and atomic
run: cargo test --features "alloc set atomic" -- --skip alloc_fuzz_tests
- name: Test Release with alloc, set and atomic
run: cargo test --release --features "alloc set atomic" -- --skip alloc_fuzz_tests
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **`SlabBStackAllocator` version bumped to 0.1.1** (`alloc` + `set` features): Magic number updated from `ALSL\x00\x01\x00\x00` to `ALSL\x00\x01\x01\x00`. Reflects the addition of `atomic` / `Sync` support. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open).
- **`CheckedSlabBStackAllocator` version bumped to 0.1.1** (`alloc` + `set` features): Magic number updated from `ALCK\x00\x01\x00\x00` to `ALCK\x00\x01\x01\x00`. Reflects the addition of `atomic` / `Sync` support. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open).
- **`SlabBStackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Without `atomic`, free-list mutations read then write `free_head` as separate `BStack` calls — a TOCTOU race that can hand the same block to two callers. With `atomic`, an internal mutex serialises free-list pop/push; tail operations use `BStack::try_discard` / `BStack::try_extend_zeros` (atomic check-and-act under `BStack`'s own write lock, no allocator lock needed). Non-tail paths lock only around `push_free_blocks`.
- **`CheckedSlabBStackAllocator` is `Send + Sync` with the `atomic` feature** (`alloc` + `set` features): Same mutex model. Free-list pop in `alloc` is lock-scoped; tail extend runs lock-free. `dealloc` uses `try_discard` for the tail path without the lock; free-list push is locked. In `realloc`, the grow path uses `try_extend_zeros` lock-free; the shrink path holds the lock across tail-check + overhead-write + discard (overhead must be committed before truncation for crash safety). `recover` holds the lock for its full duration.

---

## [0.2.3] - 2026-06-01

### Added
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ bstack = { version = "0.2", features = ["alloc", "set"] }
user data offset 48 (arena start)
```

* **`magic`** — `"ALSL\x00\x01\x00\x00"` (version 0.1).
* **`magic`** — `"ALSL\x00\x01\x01\x00"` (version 0.1.1).
* **`block_size`** — fixed size of every arena block, little-endian `u64`.
* **`free_head`** — payload offset of the first free block's first byte, or `0` (sentinel).
* **Free block** — first 8 bytes hold the payload offset of the next free block (`u64` LE, `0` = end of list); remaining bytes belong to the caller when live.
Expand Down Expand Up @@ -870,6 +870,14 @@ Slab blocks at the tail are added to the free list (not discarded) so they can b

Each free-list mutation is two `BStack` calls: write the next-pointer into the block, then update `free_head` in the header. A crash between the two calls leaks the block being added or removed but leaves the rest of the free list intact. No recovery scan is required on reopen.

#### Thread safety

`SlabBStackAllocator` is always **`Send`** — ownership can be transferred to another thread.

Without the `atomic` feature it is **not `Sync`**: free-list mutations require a read then a write of `free_head` as separate `BStack` calls — a TOCTOU race under concurrent `&self` access that can result in two callers receiving the same block.

With the `atomic` feature it **is `Sync`**. An internal mutex serialises all compound operations that span multiple `BStack` calls: free-list pop/push and the tail-length check that precedes any `extend` or `discard`. The tail-path operations themselves use `try_discard` / `try_extend_zeros`, which check-and-act atomically under `BStack`'s own write lock without needing the allocator lock.
Comment thread
williamwutq marked this conversation as resolved.

#### Constructors

| Constructor | Stack | Effect |
Expand Down Expand Up @@ -922,7 +930,7 @@ bstack = { version = "0.2", features = ["alloc", "set"] }
user data offset 48 (arena start)
```

* **`magic`** — `"ALCK\x00\x01\x00\x00"` (version 0.1).
* **`magic`** — `"ALCK\x00\x01\x01\x00"` (version 0.1.1).
* **`block_size`** — `data_size + 8`, little-endian `u64`.
* **`free_head`** — block start offset of the first free block, or `0` (sentinel; no valid block starts at offset 0).

Expand Down Expand Up @@ -963,6 +971,14 @@ Multi-block requests always extend the tail — the free list holds single block

Free-list mutations write block payloads before updating `free_head`. The overhead high bit is flipped to "in use" only after the block's data is clean, so a crash at any intermediate point leaks at most one block without corrupting the free list. A linear arena scan can reconstruct a valid free list from scratch if needed.

#### Thread safety

`CheckedSlabBStackAllocator` is always **`Send`** — ownership can be transferred to another thread.

Without the `atomic` feature it is **not `Sync`**: free-list mutations read then write `free_head` as separate `BStack` calls — a TOCTOU race under concurrent `&self` access.

With the `atomic` feature it **is `Sync`**. An internal mutex serialises all compound operations: free-list pop/push, the tail-length checks preceding `extend` or `discard`, and the `recover` scan. Tail deallocations use `try_discard` (atomically checks tail and removes bytes under `BStack`'s own write lock) without needing the allocator lock. The shrink path acquires the lock before the tail check because the overhead must be written before discarding.

#### Constructors

| Constructor | Stack | Effect |
Expand Down
30 changes: 28 additions & 2 deletions c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,20 @@ TEST_GT_OBJ = test_ghost_tree.o
TEST_SL_BIN = test_slab$(EXE_EXT)
TEST_SL_OBJ = test_slab.o

# Slab allocator test compiled with SET+ATOMIC, exercising the mutex and the
# try_discard / try_extend_zeros tail paths under concurrent access.
TEST_SL_ATOMIC_OBJ = test_slab-atomic.o
TEST_SL_ATOMIC_BIN = test_slab-atomic$(EXE_EXT)

# Checked-slab allocator test (requires BSTACK_FEATURE_SET).
TEST_CSL_BIN = test_checked_slab$(EXE_EXT)
TEST_CSL_OBJ = test_checked_slab.o

# Checked-slab allocator test compiled with SET+ATOMIC, exercising the mutex
# and the try_discard / try_extend_zeros tail paths under concurrent access.
TEST_CSL_ATOMIC_OBJ = test_checked_slab-atomic.o
TEST_CSL_ATOMIC_BIN = test_checked_slab-atomic$(EXE_EXT)

# ByteVec — requires BSTACK_FEATURE_SET.
OBJ_BYTEVEC = bstack_bytevec.o
LIB_BYTEVEC_SET = libbstack-bytevec-set.a
Expand All @@ -70,7 +80,7 @@ MOVE_COW_BIN = ../examples/move_and_cow$(EXE_EXT)
LINKED_LIST_BIN = ../examples/linked_list$(EXE_EXT)
CHECKSUMMED_CACHE_BIN = ../examples/checksummed_cache$(EXE_EXT)

.PHONY: all test test-set test-atomic test-set-atomic test-first-fit test-first-fit-atomic test-ghost-tree test-slab test-checked-slab test-bytevec leaks clean alloc \
.PHONY: all test test-set test-atomic test-set-atomic test-first-fit test-first-fit-atomic test-ghost-tree test-slab test-slab-atomic test-checked-slab test-checked-slab-atomic test-bytevec leaks clean alloc \
example example-basic example-buffer_reuse example-journal \
example-reading example-vec_store example-hashmap example-atomic_ops example-move_and_cow \
example-linked_list example-checksummed_cache
Expand Down Expand Up @@ -165,6 +175,13 @@ $(TEST_SL_BIN): $(TEST_SL_OBJ) $(LIB_ALLOC_SET)
test-slab: $(TEST_SL_BIN)
$(RUN)$(TEST_SL_BIN)

# Same slab test compiled against the SET+ATOMIC alloc lib, exercising the
# in-memory mutex and the try_discard / try_extend_zeros tail paths.
test-slab-atomic: $(LIB_ALLOC_SET_ATOMIC)
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -c -o $(TEST_SL_ATOMIC_OBJ) test_slab.c
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -o $(TEST_SL_ATOMIC_BIN) $(TEST_SL_ATOMIC_OBJ) -L. -lbstack-alloc-set-atomic -lpthread
$(RUN)$(TEST_SL_ATOMIC_BIN)
Comment thread
williamwutq marked this conversation as resolved.

$(TEST_CSL_OBJ): test_checked_slab.c bstack_alloc.h bstack.h
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -c -o $@ $<

Expand All @@ -174,6 +191,13 @@ $(TEST_CSL_BIN): $(TEST_CSL_OBJ) $(LIB_ALLOC_SET)
test-checked-slab: $(TEST_CSL_BIN)
$(RUN)$(TEST_CSL_BIN)

# Same checked-slab test compiled against the SET+ATOMIC alloc lib, exercising
# the in-memory mutex and the try_discard / try_extend_zeros tail paths.
test-checked-slab-atomic: $(LIB_ALLOC_SET_ATOMIC)
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -c -o $(TEST_CSL_ATOMIC_OBJ) test_checked_slab.c
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -DBSTACK_FEATURE_ATOMIC -o $(TEST_CSL_ATOMIC_BIN) $(TEST_CSL_ATOMIC_OBJ) -L. -lbstack-alloc-set-atomic -lpthread
$(RUN)$(TEST_CSL_ATOMIC_BIN)

$(OBJ_BYTEVEC): bstack_bytevec.c bstack_bytevec.h bstack_alloc.h bstack.h
$(CC) $(CFLAGS) -DBSTACK_FEATURE_SET -c -o $@ $<

Expand Down Expand Up @@ -305,6 +329,8 @@ clean:
test_first_fit-atomic.o test_first_fit-atomic$(EXE_EXT) \
$(TEST_GT_OBJ) $(TEST_GT_BIN) \
$(TEST_SL_OBJ) $(TEST_SL_BIN) \
$(TEST_CSL_OBJ) $(TEST_CSL_BIN) \
$(TEST_SL_ATOMIC_OBJ) $(TEST_SL_ATOMIC_BIN) \
$(TEST_CSL_OBJ) $(TEST_CSL_BIN) \
$(TEST_CSL_ATOMIC_OBJ) $(TEST_CSL_ATOMIC_BIN) \
$(OBJ_BYTEVEC) $(LIB_BYTEVEC_SET) $(TEST_BV_OBJ) $(TEST_BV_BIN) \
$(EXAMPLE_BINS) $(HASHMAP_BIN) $(ATOMIC_BIN) $(MOVE_COW_BIN) $(LINKED_LIST_BIN) $(CHECKSUMMED_CACHE_BIN)
Loading
Loading