/*! \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 #include #include // 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 free_stack; // Free buffers (stack — push/pop from back) std::vector 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(raw); auto h = raw + sizeof(POOLHDR); auto pf = reinterpret_cast(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(raw); p = reinterpret_cast(raw + sizeof(POOLHDR)); pf = reinterpret_cast(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(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(__FILE__), __LINE__); return nullptr; } pools[poolnum].all_buffers.push_back(raw); pool_account_system(pools[poolnum].pool_alloc_size); ph = reinterpret_cast(raw); p = reinterpret_cast(raw + sizeof(POOLHDR)); pf = reinterpret_cast(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(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(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(raw); p = reinterpret_cast(raw + sizeof(POOLHDR)); pf = reinterpret_cast(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(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(__FILE__), __LINE__); return nullptr; } pools[POOL_LBUF].all_buffers.push_back(raw); pool_account_system(LBUF_SIZE + sizeof(POOLHDR) + sizeof(POOLFTR)); ph = reinterpret_cast(raw); p = reinterpret_cast(raw + sizeof(POOLHDR)); pf = reinterpret_cast(p + LBUF_SIZE); ph->magicnum = pools[POOL_LBUF].poolmagic; ph->pool_size = LBUF_SIZE; pf->magicnum = pools[POOL_LBUF].poolmagic; *reinterpret_cast(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(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(buf) - sizeof(POOLHDR); POOLHDR *ph = reinterpret_cast(raw); const auto pf = reinterpret_cast(buf + pools[poolnum].pool_client_size); const auto pui = reinterpret_cast(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(buf) - sizeof(POOLHDR); POOLHDR *ph = reinterpret_cast(raw); const auto pf = reinterpret_cast(buf + LBUF_SIZE); const auto pui = reinterpret_cast(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(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(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(pools[i].pool_client_size), ' '); num[nw] = '\0'; pos = mux_table_append_bytes(buff, sizeof(buff), pos, reinterpret_cast(num)); pos = mux_table_append_bytes(buff, sizeof(buff), pos, " "); nw = RightJustifyNumber(num, kBufInUseCols, static_cast(pools[i].num_alloc), ' '); num[nw] = '\0'; pos = mux_table_append_bytes(buff, sizeof(buff), pos, reinterpret_cast(num)); pos = mux_table_append_bytes(buff, sizeof(buff), pos, " "); nw = RightJustifyNumber(num, kBufTotalCols, static_cast(pools[i].max_alloc), ' '); num[nw] = '\0'; pos = mux_table_append_bytes(buff, sizeof(buff), pos, reinterpret_cast(num)); pos = mux_table_append_bytes(buff, sizeof(buff), pos, " "); nw = RightJustifyNumber(num, kBufAllocsCols, static_cast(pools[i].tot_alloc), ' '); num[nw] = '\0'; pos = mux_table_append_bytes(buff, sizeof(buff), pos, reinterpret_cast(num)); pos = mux_table_append_bytes(buff, sizeof(buff), pos, " "); nw = RightJustifyNumber(num, kBufLostCols, static_cast(pools[i].num_lost), ' '); num[nw] = '\0'; pos = mux_table_append_bytes(buff, sizeof(buff), pos, reinterpret_cast(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 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; } }