diff --git a/xo-arena/include/xo/arena/DArena.hpp b/xo-arena/include/xo/arena/DArena.hpp index f485b3de..5148ba6e 100644 --- a/xo-arena/include/xo/arena/DArena.hpp +++ b/xo-arena/include/xo/arena/DArena.hpp @@ -241,6 +241,22 @@ namespace xo { **/ void clear() noexcept; + /** release backing memory and reset bookkeeping to the empty state. + * + * Unmaps [@ref lo_, @ref hi_) (if mapped) and zeroes the bookkeeping + * fields {lo_, committed_z_, last_header_, free_, limit_, hi_, + * error_count_, last_error_}. @ref config_ (and page_z_/arena_align_z_) + * are left intact. + * + * Idempotent: a second call is a no-op (lo_ has been cleared). + * Invoked by ~DArena(); also safe to call on a live arena. + * + * Note: application code must not rely on observing the zeroed state + * after destruction (that would be UB) -- the zeroing is defensive, + * to avoid leaving dangling pointers behind. + **/ + void unmap() noexcept; + /** swap contents (including configuration) with another arena **/ void swap(DArena & other) noexcept; diff --git a/xo-arena/src/arena/DArena.cpp b/xo-arena/src/arena/DArena.cpp index 0c429558..ccd6d52e 100644 --- a/xo-arena/src/arena/DArena.cpp +++ b/xo-arena/src/arena/DArena.cpp @@ -13,6 +13,7 @@ #include #include #include +#include // for std::launder() #include // for ::munmap() #include // for ::getpagesize() #include // for ::memset() @@ -142,7 +143,8 @@ namespace xo { return *this; } - DArena::~DArena() + void + DArena::unmap() noexcept { if (lo_) { //log && log("unmap [lo,hi)", @@ -153,15 +155,27 @@ namespace xo { ::munmap(lo_, hi_ - lo_); } - // hygiene - lo_ = nullptr; - committed_z_ = 0; - // checkpoint_ = nullptr; - free_ = nullptr; - limit_ = nullptr; - hi_ = nullptr; - error_count_ = 0; - last_error_ = AllocError(); + /* Mandatory hygiene: zero the bookkeeping tail so no dangling + * pointers survive (e.g. after ~DArena() runs the dtor of a + * stack-owned arena). config_, page_z_, arena_align_z_ are + * preserved -- only {lo_ .. last_error_} get cleared. + * + * The memset goes through a std::launder'd self pointer so the + * compiler cannot prove it writes to the dying object and elide + * the stores as dead (gcc>=15 does that with a plain member + * assignment / un-laundered memset at -O1). All cleared fields + * are trivially-copyable, so byte-zeroing them is well-defined. + */ + DArena * self = std::launder(this); + byte * tail = reinterpret_cast(&self->lo_); + byte * end = reinterpret_cast(self) + sizeof(DArena); + + ::memset(tail, 0, end - tail); + } + + DArena::~DArena() + { + this->unmap(); } auto