tinymux/mux/lib/alloc.cpp
Stephen Dennis 5f43951f46 fix(tables): pot for C* labels; no %* width on mux_sprintf
Regenerate pot/xx/ko after Phase 4 C* (drop blob headers; add per-label
msgids).  Row right-just must use RightJustifyNumber — mux_vsnprintf does
not implement %* width (#1429), so the filed %*d/%*lld path would echo
literally and drop the rest of each stats line.
2026-07-28 17:34:14 +00:00

696 lines
23 KiB
C++

/*! \file alloc.cpp
* \brief Memory Allocation Subsystem.
*
* The functions here manage pools of often-used buffers of fixed-size. It
* adds value by greatly reducing the number and strength of calls to the
* underlying platform's memory management. Headers and footers detect
* misuse of buffers by callers.
*
* Buffer tracking and freelists use std::vector instead of intrusive
* linked lists, eliminating the next/nxtfree pointers from POOLHDR.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "core.h"
#include "modules.h"
#include "mux_table.h"
#include <algorithm>
#include <unordered_set>
#include <vector>
// libmux.so cannot access mudconf directly (it lives in engine.so).
// This flag mirrors g_paranoid_alloc; the engine sets it after
// loading configuration via g_paranoid_alloc in alloc.h (future work:
// libmux config broadcast). For now, default to false.
//
bool g_paranoid_alloc = false;
// Pool memory footprint budget (#pool-oom). See alloc.h.
size_t g_pool_limit_bytes = 0;
size_t g_pool_system_bytes = 0;
// Engine → driver live config push. See alloc.h.
DRIVER_CONFIG_SYNC_FN g_driver_config_sync_fn = nullptr;
// Account bytes just taken from the system on a pool slow-path alloc and, if
// they push the footprint past the budget, trip the per-command abort so the
// current (runaway) command unwinds and frees its buffers back to the freelist
// — a graceful degrade instead of a fatal OutOfMemory.
//
// Policy: the budget is a permanent soft ceiling on system growth. Pool
// freelist returns never reduce g_pool_system_bytes (buffers stay owned by
// the pool), so once past the limit the process stays over budget until
// restart or the admin raises pool_memory_limit. Log once per breach; do
// not re-arm the warning from freelist activity (there is none that lowers
// the counter). After the first breach, continue to trip alarm_clock on
// further slow-path growth so subsequent runaway allocs still unwind rather
// than growing without bound.
//
static void pool_account_system(size_t nBytes)
{
static bool warned = false;
g_pool_system_bytes += nBytes;
if (0 != g_pool_limit_bytes)
{
if (g_pool_system_bytes > g_pool_limit_bytes)
{
alarm_clock.alarmed.store(true, std::memory_order_relaxed);
if (!warned)
{
warned = true;
mux_fprintf(stderr,
T("Pool memory budget exceeded (%zu > %zu bytes); aborting "
"the current command to protect the server. Further "
"slow-path growth will keep aborting until the budget "
"is raised or the process restarts." ENDLINE),
g_pool_system_bytes, g_pool_limit_bytes);
}
}
// Re-arm only when the admin raises the limit past the current
// footprint (g_pool_system_bytes never shrinks).
else if (warned && g_pool_system_bytes <= g_pool_limit_bytes)
{
warned = false;
}
}
}
// Output callback for @list buffers — set by engine at startup.
//
ALLOC_NOTIFY_FN g_alloc_notify_fn = nullptr;
/*! \brief Per-buffer header to manage and organize client allocation.
*
* The POOLHDR structure precedes a client area which must be properly
* aligned to avoid faults when the client accesses structure members within
* the client area. 64-bit alignment should be sufficient.
*
* The magicnum and pool_size are chosen when the pool is initialized.
* buf_tag comes from the client so that buffers can be associated with the
* places that allocated them.
*/
typedef struct pool_header
{
unsigned int magicnum; // For consistency check
size_t pool_size; // For consistency check
union
{
const UTF8 *buf_tag; // Debugging/trace tag
uint64_t align; // Alignment padding
} u;
} POOLHDR;
/*! \brief Per-buffer footer to catch buffer overruns.
*
* The POOLFTR structure helps detect when a client has written beyond the
* bounds of the buffer.
*/
typedef struct pool_footer
{
unsigned int magicnum; // For consistency check
} POOLFTR;
/*! \brief Per-pool structure containing statistics and vector-based lists.
*
* The freelist is a stack (vector) of raw allocation pointers.
* all_buffers tracks every allocation for diagnostics and cleanup.
*/
typedef struct pooldata
{
size_t pool_client_size; // Size in bytes of a buffer as seen by client.
size_t pool_alloc_size; // Size as allocated from system.
unsigned int poolmagic; // Magic number specific to this pool
std::vector<char *> free_stack; // Free buffers (stack — push/pop from back)
std::vector<char *> all_buffers;// All allocated raw blocks (for diagnostics)
uint64_t tot_alloc; // Total buffers allocated
uint64_t num_alloc; // Number of buffers currently allocated
uint64_t max_alloc; // Max # buffers allocated at one time
uint64_t num_lost; // Buffers lost due to corruption
} POOL;
static POOL pools[NUM_POOLS];
static const UTF8 *poolnames[] =
{
T("Lbufs"),
T("Sbufs"),
T("Mbufs"),
T("Bools"),
T("Descs"),
T("Qentries"),
T("Pcaches")
};
/*! \brief Initialize a buffer pool.
*
* This is done once. The client size, magic, and allocation size are chosen
* at this time. After this initialization, allocations can be done.
*
* \param poolnum An integer uniquely indicating which pool.
* \param poolsize The size of the client area this pool supports.
*/
void pool_init(int poolnum, int poolsize)
{
pools[poolnum].pool_client_size = poolsize;
pools[poolnum].pool_alloc_size = poolsize + sizeof(POOLHDR) + sizeof(POOLFTR);
mux_assert(pools[poolnum].pool_client_size < pools[poolnum].pool_alloc_size);
pools[poolnum].poolmagic = CRC32_ProcessInteger2(poolnum, poolsize);
pools[poolnum].free_stack.clear();
pools[poolnum].all_buffers.clear();
pools[poolnum].tot_alloc = 0;
pools[poolnum].num_alloc = 0;
pools[poolnum].max_alloc = 0;
pools[poolnum].num_lost = 0;
}
/*! \brief Helper function for logging pool errors.
*/
static void pool_err
(
const UTF8 *logsys,
int logflag,
int poolnum,
const UTF8 *tag,
POOLHDR* ph,
const UTF8 *action,
const UTF8 *reason,
const UTF8 *file,
const int line
)
{
UNUSED_PARAMETER(logflag);
mux_fprintf(stderr, T("%s %s[%d] (tag %s) %s in %s line %d at %p." ENDLINE),
logsys, action, pools[poolnum].pool_client_size, tag, reason,
file, line, ph);
}
/*! \brief Validates the buffers in the all_buffers list.
*
* Walks all allocated buffers and checks that magic numbers are correct.
* Reports errors but does not unlink or leak buffers — that strategy was
* a relic of the intrusive-list era.
*/
static void pool_vfy
(
int poolnum,
const UTF8 *tag,
const UTF8 *file,
const int line
)
{
const size_t psize = pools[poolnum].pool_client_size;
for (char *raw : pools[poolnum].all_buffers)
{
auto ph = reinterpret_cast<POOLHDR *>(raw);
auto h = raw + sizeof(POOLHDR);
auto pf = reinterpret_cast<POOLFTR *>(h + pools[poolnum].pool_client_size);
if (ph->magicnum != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, tag, ph, T("Verify"),
T("header corrupted"), file, line);
continue;
}
if (pf->magicnum != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, tag, ph, T("Verify"),
T("footer corrupted"), file, line);
pf->magicnum = pools[poolnum].poolmagic;
}
if (ph->pool_size != psize)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, tag, ph,
T("Verify"), T("header has incorrect size"), file, line);
}
}
}
static void pool_check(const UTF8 *tag, const UTF8 *file, const int line)
{
for (int i = 0; i < NUM_POOLS; i++)
{
pool_vfy(i, tag, file, line);
}
}
UTF8 *pool_alloc(int poolnum, const UTF8 *tag, const UTF8 *file, const int line)
{
if (g_paranoid_alloc)
{
pool_check(tag, file, line);
}
UTF8 *p;
POOLHDR *ph;
POOLFTR *pf;
// Walk freelist; drop only corrupt entries rather than clearing the
// entire freelist (old path leaked every still-valid free buffer —
// Pass H1 residual #1290).
//
while (!pools[poolnum].free_stack.empty())
{
char *raw = pools[poolnum].free_stack.back();
pools[poolnum].free_stack.pop_back();
ph = reinterpret_cast<POOLHDR *>(raw);
p = reinterpret_cast<UTF8 *>(raw + sizeof(POOLHDR));
pf = reinterpret_cast<POOLFTR *>(p + pools[poolnum].pool_client_size);
if (ph->magicnum != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, tag, ph, T("Alloc"),
T("corrupted buffer header on freelist"), file, line);
pools[poolnum].num_lost++;
// raw is abandoned (still tracked in all_buffers for diagnostics).
continue;
}
// Check for corrupted footer, just report and fix it.
//
if (pf->magicnum != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, tag, ph, T("Alloc"),
T("corrupted buffer footer"), file, line);
pf->magicnum = pools[poolnum].poolmagic;
}
ph->u.buf_tag = tag;
pools[poolnum].tot_alloc++;
pools[poolnum].num_alloc++;
// If the buffer was modified after it was last freed, log it.
//
auto pui = reinterpret_cast<unsigned *>(p);
if (*pui != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_PROBLEMS, poolnum, tag, ph, T("Alloc"),
T("buffer modified after free"), file, line);
}
*pui = 0;
return p;
}
// Allocate a new buffer from the system.
//
char *raw = nullptr;
try
{
raw = new char[pools[poolnum].pool_alloc_size];
}
catch (...)
{
}
if (nullptr == raw)
{
OutOfMemory(reinterpret_cast<const UTF8 *>(__FILE__), __LINE__);
return nullptr;
}
pools[poolnum].all_buffers.push_back(raw);
pool_account_system(pools[poolnum].pool_alloc_size);
ph = reinterpret_cast<POOLHDR *>(raw);
p = reinterpret_cast<UTF8 *>(raw + sizeof(POOLHDR));
pf = reinterpret_cast<POOLFTR *>(p + pools[poolnum].pool_client_size);
ph->magicnum = pools[poolnum].poolmagic;
ph->pool_size = pools[poolnum].pool_client_size;
pf->magicnum = pools[poolnum].poolmagic;
*reinterpret_cast<unsigned *>(p) = pools[poolnum].poolmagic;
pools[poolnum].max_alloc++;
ph->u.buf_tag = tag;
pools[poolnum].tot_alloc++;
pools[poolnum].num_alloc++;
auto pui = reinterpret_cast<unsigned *>(p);
*pui = 0;
return p;
}
UTF8 *pool_alloc_lbuf(const UTF8 *tag, const UTF8 *file, const int line)
{
if (g_paranoid_alloc)
{
pool_check(tag, file, line);
}
UTF8 *p;
POOLHDR *ph;
POOLFTR *pf;
// Same freelist discipline as pool_alloc: drop only the bad entry (#1290).
//
while (!pools[POOL_LBUF].free_stack.empty())
{
char *raw = pools[POOL_LBUF].free_stack.back();
pools[POOL_LBUF].free_stack.pop_back();
ph = reinterpret_cast<POOLHDR *>(raw);
p = reinterpret_cast<UTF8 *>(raw + sizeof(POOLHDR));
pf = reinterpret_cast<POOLFTR *>(p + LBUF_SIZE);
if (ph->magicnum != pools[POOL_LBUF].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, POOL_LBUF, tag, ph, T("Alloc"),
T("corrupted buffer header on freelist"), file, line);
pools[POOL_LBUF].num_lost++;
continue;
}
if (pf->magicnum != pools[POOL_LBUF].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, POOL_LBUF, tag, ph, T("Alloc"),
T("corrupted buffer footer"), file, line);
pf->magicnum = pools[POOL_LBUF].poolmagic;
}
ph->u.buf_tag = tag;
pools[POOL_LBUF].tot_alloc++;
pools[POOL_LBUF].num_alloc++;
auto pui = reinterpret_cast<unsigned *>(p);
if (*pui != pools[POOL_LBUF].poolmagic)
{
pool_err(T("BUG"), LOG_PROBLEMS, POOL_LBUF, tag, ph, T("Alloc"),
T("buffer modified after free"), file, line);
}
*pui = 0;
return p;
}
char *raw = nullptr;
try
{
raw = new char[LBUF_SIZE + sizeof(POOLHDR) + sizeof(POOLFTR)];
}
catch (...)
{
}
if (nullptr == raw)
{
OutOfMemory(reinterpret_cast<const UTF8 *>(__FILE__), __LINE__);
return nullptr;
}
pools[POOL_LBUF].all_buffers.push_back(raw);
pool_account_system(LBUF_SIZE + sizeof(POOLHDR) + sizeof(POOLFTR));
ph = reinterpret_cast<POOLHDR *>(raw);
p = reinterpret_cast<UTF8 *>(raw + sizeof(POOLHDR));
pf = reinterpret_cast<POOLFTR *>(p + LBUF_SIZE);
ph->magicnum = pools[POOL_LBUF].poolmagic;
ph->pool_size = LBUF_SIZE;
pf->magicnum = pools[POOL_LBUF].poolmagic;
*reinterpret_cast<unsigned *>(p) = pools[POOL_LBUF].poolmagic;
pools[POOL_LBUF].max_alloc++;
ph->u.buf_tag = tag;
pools[POOL_LBUF].tot_alloc++;
pools[POOL_LBUF].num_alloc++;
auto pui = reinterpret_cast<unsigned *>(p);
*pui = 0;
return p;
}
void pool_free(int poolnum, UTF8* buf, const UTF8* file, const int line)
{
if (buf == nullptr)
{
mux_fprintf(stderr, T("BUG ALLOC: Attempt to free null pointer in %s line %d." ENDLINE), file, line);
return;
}
char *raw = reinterpret_cast<char *>(buf) - sizeof(POOLHDR);
POOLHDR *ph = reinterpret_cast<POOLHDR *>(raw);
const auto pf = reinterpret_cast<POOLFTR *>(buf + pools[poolnum].pool_client_size);
const auto pui = reinterpret_cast<unsigned *>(buf);
if (g_paranoid_alloc)
{
pool_check(ph->u.buf_tag, file, line);
}
// Make sure the buffer header is good. If it isn't, log the error and
// throw away the buffer.
//
if (ph->magicnum != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, ph->u.buf_tag, ph, T("Free"),
T("corrupted buffer header"), file, line);
pools[poolnum].num_lost++;
pools[poolnum].num_alloc--;
pools[poolnum].tot_alloc--;
return;
}
// Verify the buffer footer. Don't unlink if damaged, just repair.
//
if (pf->magicnum != pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, ph->u.buf_tag, ph, T("Free"),
T("corrupted buffer footer"), file, line);
pf->magicnum = pools[poolnum].poolmagic;
}
// Verify that we are not trying to free someone else's buffer.
//
if (ph->pool_size != pools[poolnum].pool_client_size)
{
pool_err(T("BUG"), LOG_ALWAYS, poolnum, ph->u.buf_tag, ph, T("Free"),
T("Attempt to free into a different pool."), file, line);
return;
}
// Make sure we aren't freeing an already free buffer. If we are, log an
// error, otherwise update the pool header and stats.
//
if (*pui == pools[poolnum].poolmagic)
{
pool_err(T("BUG"), LOG_BUGS, poolnum, ph->u.buf_tag, ph, T("Free"),
T("buffer already freed"), file, line);
}
else
{
*pui = pools[poolnum].poolmagic;
pools[poolnum].free_stack.push_back(raw);
pools[poolnum].num_alloc--;
}
}
void pool_free_lbuf(UTF8 *buf, const UTF8 *file, const int line)
{
if (buf == nullptr)
{
mux_fprintf(stderr, T("BUG ALLOC: Attempt to free_lbuf null pointer in %s line %d." ENDLINE), file, line);
return;
}
char *raw = reinterpret_cast<char *>(buf) - sizeof(POOLHDR);
POOLHDR *ph = reinterpret_cast<POOLHDR *>(raw);
const auto pf = reinterpret_cast<POOLFTR *>(buf + LBUF_SIZE);
const auto pui = reinterpret_cast<unsigned *>(buf);
if (g_paranoid_alloc)
{
pool_check(ph->u.buf_tag, file, line);
}
if ( ph->magicnum != pools[POOL_LBUF].poolmagic
|| pf->magicnum != pools[POOL_LBUF].poolmagic
|| ph->pool_size != LBUF_SIZE
|| *pui == pools[POOL_LBUF].poolmagic)
{
if (ph->magicnum != pools[POOL_LBUF].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, POOL_LBUF, ph->u.buf_tag, ph, T("Free"),
T("corrupted buffer header"), file, line);
pools[POOL_LBUF].num_lost++;
pools[POOL_LBUF].num_alloc--;
pools[POOL_LBUF].tot_alloc--;
return;
}
else if (pf->magicnum != pools[POOL_LBUF].poolmagic)
{
pool_err(T("BUG"), LOG_ALWAYS, POOL_LBUF, ph->u.buf_tag, ph, T("Free"),
T("corrupted buffer footer"), file, line);
pf->magicnum = pools[POOL_LBUF].poolmagic;
}
else if (ph->pool_size != LBUF_SIZE)
{
pool_err(T("BUG"), LOG_ALWAYS, POOL_LBUF, ph->u.buf_tag, ph, T("Free"),
T("Attempt to free into a different pool."), file, line);
return;
}
if (*pui == pools[POOL_LBUF].poolmagic)
{
pool_err(T("BUG"), LOG_BUGS, POOL_LBUF, ph->u.buf_tag, ph, T("Free"),
T("buffer already freed"), file, line);
return;
}
}
*pui = pools[POOL_LBUF].poolmagic;
pools[POOL_LBUF].free_stack.push_back(raw);
pools[POOL_LBUF].num_alloc--;
}
static inline void alloc_notify(dbref player, const UTF8 *msg)
{
if (g_alloc_notify_fn)
{
g_alloc_notify_fn(player, msg);
}
}
static void pool_trace(const dbref player, const int poolnum, const UTF8 *text)
{
int numfree = 0;
alloc_notify(player, tprintf(T("----- %s -----"), text));
for (char *raw : pools[poolnum].all_buffers)
{
auto ph = reinterpret_cast<POOLHDR *>(raw);
if (ph->magicnum != pools[poolnum].poolmagic)
{
alloc_notify(player, M_("*** CORRUPTED BUFFER HEADER, ABORTING SCAN ***"));
alloc_notify(player, tprintf(T("%d free %s (before corruption)"),
numfree, text));
return;
}
auto ibuf = reinterpret_cast<unsigned *>(raw + sizeof(POOLHDR));
if (*ibuf != pools[poolnum].poolmagic)
{
alloc_notify(player, ph->u.buf_tag);
}
else
{
numfree++;
}
}
alloc_notify(player, tprintf(T("%d free %s"), numfree, text));
}
void list_bufstats(dbref player)
{
// Buffer pool table (#1667 Phase 4 C4). Name is left-justified; numeric
// columns keep fixed-width right justification via RightJustifyNumber
// (mux_vsnprintf has no %* width — #1429).
//
static const size_t kBufNameCols = 12;
static const size_t kBufSizeCols = 5;
static const size_t kBufInUseCols = 10;
static const size_t kBufTotalCols = 10;
static const size_t kBufAllocsCols = 16;
static const size_t kBufLostCols = 6;
{
UTF8 header[MBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Buffer Stats"), kBufNameCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Size"), kBufSizeCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("InUse"), kBufInUseCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Total"), kBufTotalCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Allocs"), kBufAllocsCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Lost"), kBufLostCols);
alloc_notify(player, header);
}
for (int i = 0; i < NUM_POOLS; i++)
{
UTF8 buff[MBUF_SIZE];
UTF8 num[24];
size_t pos = 0;
size_t nw;
pos = mux_table_append_ljust(buff, sizeof(buff), pos,
poolnames[i], kBufNameCols);
pos = mux_table_append_bytes(buff, sizeof(buff), pos, " ");
nw = RightJustifyNumber(num, kBufSizeCols,
static_cast<int64_t>(pools[i].pool_client_size), ' ');
num[nw] = '\0';
pos = mux_table_append_bytes(buff, sizeof(buff), pos,
reinterpret_cast<const char *>(num));
pos = mux_table_append_bytes(buff, sizeof(buff), pos, " ");
nw = RightJustifyNumber(num, kBufInUseCols,
static_cast<int64_t>(pools[i].num_alloc), ' ');
num[nw] = '\0';
pos = mux_table_append_bytes(buff, sizeof(buff), pos,
reinterpret_cast<const char *>(num));
pos = mux_table_append_bytes(buff, sizeof(buff), pos, " ");
nw = RightJustifyNumber(num, kBufTotalCols,
static_cast<int64_t>(pools[i].max_alloc), ' ');
num[nw] = '\0';
pos = mux_table_append_bytes(buff, sizeof(buff), pos,
reinterpret_cast<const char *>(num));
pos = mux_table_append_bytes(buff, sizeof(buff), pos, " ");
nw = RightJustifyNumber(num, kBufAllocsCols,
static_cast<int64_t>(pools[i].tot_alloc), ' ');
num[nw] = '\0';
pos = mux_table_append_bytes(buff, sizeof(buff), pos,
reinterpret_cast<const char *>(num));
pos = mux_table_append_bytes(buff, sizeof(buff), pos, " ");
nw = RightJustifyNumber(num, kBufLostCols,
static_cast<int64_t>(pools[i].num_lost), ' ');
num[nw] = '\0';
pos = mux_table_append_bytes(buff, sizeof(buff), pos,
reinterpret_cast<const char *>(num));
alloc_notify(player, buff);
}
}
void list_buftrace(dbref player)
{
for (int i = 0; i < NUM_POOLS; i++)
{
pool_trace(player, i, poolnames[i]);
}
}
void pool_reset(void)
{
for (auto &pool : pools)
{
// Build a set of free buffer pointers for fast lookup.
//
std::unordered_set<char *> free_set(
pool.free_stack.begin(), pool.free_stack.end());
pool.free_stack.clear();
// Walk all_buffers: delete free ones, keep in-use ones.
//
auto new_end = std::remove_if(pool.all_buffers.begin(),
pool.all_buffers.end(),
[&free_set](char *raw) -> bool
{
if (free_set.count(raw))
{
delete[] raw;
return true;
}
return false;
});
pool.all_buffers.erase(new_end, pool.all_buffers.end());
pool.max_alloc = pool.num_alloc;
}
}