Skip to content

multislab: iteration and free-space tracking share one list #4

Closed

Description

@aotodev

next/prev carry both iteration order and the active/full split. The iterator walks active_, then jumps to the full_ head captured at construction (second_list_, multislab.cppm:272). Any relink breaks live iterators:

  • Erase from a full slab: move_to_active prepends it. An iterator inside follows next into active_, revisits nodes and never reaches the rest of full_.
  • Insert that fills a slab: move_to_full. An iterator inside follows next into the old full_, then walks second_list_ again.
  • Slab held as second_list_ emptied and freed: use-after-free.

Separately, allocate_raw (:403) moves a full head to full_ and grows unconditionally. deallocate prepends to active_, so slabs with free slots can sit below a full head: over-growth without a cap, spurious failure with one. cfiber fixed this in 1fd1edf; not ported.

Repro (confirmed, ASan)

libmem::pool<int, 4> p;   // insert 0..11: active [C], full [B, A]
for (auto it = p.begin(); it != p.end();)
    it = (*it % 2 == 0) ? p.erase(it) : std::next(it);
// visits 10 of 12, two evens remain

Insert while iterating (11 elements, insert on visiting 8 and 9): 8 duplicate visits.

libmem::pool<int, 4> p{libmem::threshold_policy{.max_empty_reserve = 0}};
// insert 0..11, it = p.begin() (in C), erase 4..7 (B freed)
for (; it != p.end(); ++it) {}   // heap-use-after-free
libmem::pool<int, 4> p{libmem::slab_limit{2}};
// insert 0..4, erase 0, insert 100, insert 101 -> end(); B has 3 free
// uncapped: 3 slabs where 2 suffice

Fix

  • Permanent iteration links per node (iter_next/iter_prev), changed only on grow and free. The iterator holds node + slab position; drop second_list_ and on_second_list_.
  • next/prev track free space only.
  • Move a node to full_ eagerly, when used == BlocksPerSlab after an allocation. Invariant: every node on active_ has a free slot, so allocate_raw needs no retry. (Alternative: loop over active_ before growing, as cfiber does.)
  • on_full becomes used == BlocksPerSlab: keep or derive.
  • Drop the empty_count_ > 0 guard (:429), the count is exact.

Tests

The four repros. Across erase during iteration, every element live for the whole traversal is visited exactly once. Across insert, pre-existing elements exactly once, new ones at most once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions