mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
safe_copy_chr_ascii evaluated src only INSIDE the bounds test, so the
argument stopped being evaluated once the buffer filled. That makes the
macro unsafe for any side-effecting argument, and the natural loop
while (p < end) safe_chr(*p++, buff, bufp);
would spin forever instead of truncating, because p stops advancing
exactly when the copy stops happening.
This is #1930's shape. color_ops' WP_SAFE had the identical defect and
one softcode expression could hang the whole single-threaded server;
twelve loops there passed *s++ to it. safe_chr is the equivalent macro
in the main tree -- safe_chr, safe_sb_chr, safe_mb_chr and safe_bool all
expand to this -- so the same trap existed here.
It is a trap nobody is standing on: every call site of all five entry
points was checked and none passes a side-effecting first argument. The
temporary is so that stepping on it later is harmless rather than fatal.
Worth recording why the suite could not have caught this: tests/libmux
does not include alloc.h, it REPLICATES safe_chr -- as an inline
function. A function evaluates its argument exactly once by definition,
so the stand-in never had the defect and test_safe_chr could never have
found it in the real macro. The suite was green because it was testing
the good version. This change makes the shipped macro match the
semantics the test already assumed.
make test: 30 targets, 29 passed, 1 skipped (test-stubslave-teardown,
expected without --enable-stubslave).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
310 lines
12 KiB
C++
310 lines
12 KiB
C++
/*! \file alloc.h
|
|
* \brief External definitions for memory allocation subsystem.
|
|
*
|
|
*/
|
|
|
|
#ifndef ALLOC_H
|
|
#define ALLOC_H
|
|
|
|
constexpr int POOL_LBUF = 0;
|
|
constexpr int POOL_SBUF = 1;
|
|
constexpr int POOL_MBUF = 2;
|
|
constexpr int POOL_BOOL = 3;
|
|
constexpr int POOL_DESC = 4;
|
|
constexpr int POOL_QENTRY = 5;
|
|
constexpr int POOL_PCACHE = 6;
|
|
constexpr int NUM_POOLS = 7;
|
|
|
|
#define LBUF_SIZE 32768 // Large (must remain #define for preprocessor conditionals)
|
|
constexpr int GBUF_SIZE = 1024; // Generic
|
|
constexpr int MBUF_SIZE = 400; // Medium
|
|
constexpr int PBUF_SIZE = 128; // Pathname
|
|
constexpr int SBUF_SIZE = 64; // Small
|
|
|
|
extern LIBMUX_API bool g_paranoid_alloc;
|
|
|
|
// Pool memory footprint budget (#pool-oom). Buffers taken from the system
|
|
// are never returned to it (they stay on the pool freelist), so the process's
|
|
// pool footprint only grows on the slow allocation path and g_pool_system_bytes
|
|
// never shrinks. When g_pool_limit_bytes is non-zero and the running system
|
|
// total would exceed it, the allocator trips the per-command abort
|
|
// (alarm_clock.alarmed) so a runaway command unwinds and frees its buffers to
|
|
// the freelist instead of the process reaching a fatal OutOfMemory. Once
|
|
// breached the ceiling stays in force until restart or the admin raises the
|
|
// limit (the warning re-arms only if the limit is raised past the footprint).
|
|
// 0 = unlimited (legacy behavior). Set by the engine from config.
|
|
extern LIBMUX_API size_t g_pool_limit_bytes;
|
|
extern LIBMUX_API size_t g_pool_system_bytes; // total bytes ever taken from system
|
|
LIBMUX_API void pool_init(int, int);
|
|
LIBMUX_API UTF8* pool_alloc(int poolnum, const UTF8* tag, const UTF8* file, const int line);
|
|
LIBMUX_API UTF8* pool_alloc_lbuf(const UTF8* tag, const UTF8* file, const int line);
|
|
LIBMUX_API void pool_free(int poolnum, UTF8* buf, const UTF8* file, const int line);
|
|
LIBMUX_API void pool_free_lbuf(UTF8* buf, const UTF8* file, const int line);
|
|
// alloc_notify_fn — callback for @list buffers output.
|
|
// Engine sets this to a function that calls notify().
|
|
//
|
|
typedef void (*ALLOC_NOTIFY_FN)(dbref player, const UTF8 *text);
|
|
extern LIBMUX_API ALLOC_NOTIFY_FN g_alloc_notify_fn;
|
|
|
|
// driver_config_sync_fn — callback so engine @admin of DRIVER_CONFIG knobs
|
|
// can push mudconf into the driver's g_dc basket without a restart. Driver
|
|
// registers this after the first GetConfig(); engine calls it from
|
|
// cf_live_driver_int. nullptr until the driver is up (boot conf is applied
|
|
// by the initial GetConfig after LoadGame).
|
|
//
|
|
typedef void (*DRIVER_CONFIG_SYNC_FN)(void);
|
|
extern LIBMUX_API DRIVER_CONFIG_SYNC_FN g_driver_config_sync_fn;
|
|
|
|
LIBMUX_API void list_bufstats(dbref);
|
|
LIBMUX_API void list_buftrace(dbref);
|
|
LIBMUX_API void pool_reset(void);
|
|
|
|
#define alloc_lbuf(s) pool_alloc_lbuf(T(s), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define free_lbuf(b) pool_free_lbuf(reinterpret_cast<UTF8 *>(b), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define alloc_mbuf(s) pool_alloc(POOL_MBUF, T(s), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define free_mbuf(b) pool_free(POOL_MBUF, reinterpret_cast<UTF8 *>(b), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define alloc_sbuf(s) pool_alloc(POOL_SBUF, T(s), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define free_sbuf(b) pool_free(POOL_SBUF, reinterpret_cast<UTF8 *>(b), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define alloc_bool(s) reinterpret_cast<struct boolexp *>(pool_alloc(POOL_BOOL, T(s), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__))
|
|
#define free_bool(b) pool_free(POOL_BOOL, reinterpret_cast<UTF8 *>(b), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define alloc_qentry(s) reinterpret_cast<BQUE *>(pool_alloc(POOL_QENTRY, T(s), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__))
|
|
#define free_qentry(b) pool_free(POOL_QENTRY, reinterpret_cast<UTF8 *>(b), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define alloc_pcache(s) reinterpret_cast<PCACHE *>(pool_alloc(POOL_PCACHE, T(s), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__))
|
|
#define free_pcache(b) pool_free(POOL_PCACHE, reinterpret_cast<UTF8 *>(b), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
|
|
// src is evaluated exactly once, and BEFORE the bounds test.
|
|
//
|
|
// It used to be evaluated only inside the test, which makes the macro
|
|
// unsafe for any argument with a side effect: once the buffer is full the
|
|
// argument stops being evaluated at all. A caller writing the natural
|
|
//
|
|
// while (p < end) safe_chr(*p++, buff, bufp);
|
|
//
|
|
// would then spin forever rather than truncate, because p stops advancing
|
|
// exactly when the copy stops happening. That is not hypothetical -- it is
|
|
// #1930, where color_ops' WP_SAFE had this shape and one softcode
|
|
// expression could hang the whole (single-threaded) server. Twelve loops
|
|
// there passed *s++ to it.
|
|
//
|
|
// No caller here passes a side-effecting argument today; every call site of
|
|
// safe_chr / safe_sb_chr / safe_mb_chr / safe_bool was checked. This is a
|
|
// trap with nobody standing on it, and the point of the temporary is that
|
|
// stepping on it later is harmless.
|
|
#define safe_copy_chr_ascii(src, buff, bufp, nSizeOfBuffer) \
|
|
{ \
|
|
const UTF8 scca_chr_ = static_cast<UTF8>(src); \
|
|
if (static_cast<size_t>(*bufp - buff) < nSizeOfBuffer) \
|
|
{ \
|
|
**bufp = scca_chr_; \
|
|
(*bufp)++; \
|
|
} \
|
|
}
|
|
|
|
#define safe_str(s,b,p) safe_copy_str_lbuf(s,b,p)
|
|
#define safe_bool(c,b,p) safe_chr(((c) ? '1' : '0'),b,p)
|
|
#define safe_sb_str(s,b,p) safe_copy_str(s,b,p,(SBUF_SIZE-1))
|
|
#define safe_mb_str(s,b,p) safe_copy_str(s,b,p,(MBUF_SIZE-1))
|
|
|
|
#define safe_chr_ascii(c,b,p) safe_copy_chr_ascii(static_cast<UTF8>(c),b,p,(LBUF_SIZE-1))
|
|
#define safe_sb_chr_ascii(c,b,p) safe_copy_chr_ascii(c,b,p,(SBUF_SIZE-1))
|
|
#define safe_mb_chr_ascii(c,b,p) safe_copy_chr_ascii(c,b,p,(MBUF_SIZE-1))
|
|
|
|
// Slowly transition from safe_chr to safe_chr_ascii and safe_chr_utf8,
|
|
// safe_sb_chr to safe_sb_chr_ascii,and safe_mb_chr to safe_mb_chr_ascii.
|
|
//
|
|
#define safe_sb_chr safe_sb_chr_ascii
|
|
#define safe_mb_chr safe_mb_chr_ascii
|
|
#define safe_chr safe_chr_ascii
|
|
|
|
// ---------------------------------------------------------------
|
|
// RAII wrapper for pool-allocated LBUF buffers.
|
|
//
|
|
// Replaces stack-allocated UTF8 buf[LBUF_SIZE] with a heap-backed
|
|
// pool buffer that is automatically freed when the scope exits.
|
|
// This removes LBUF_SIZE from the stack frame, making it safe to
|
|
// increase LBUF_SIZE without risking stack overflow in recursive
|
|
// evaluation paths.
|
|
//
|
|
// Usage:
|
|
// LBuf tmp(T("my_func"));
|
|
// UTF8 *bp = tmp;
|
|
// safe_str(text, tmp, &bp);
|
|
// *bp = '\0';
|
|
//
|
|
// The LBuf_Src macro captures __FILE__/__LINE__ for pool tracking:
|
|
// LBuf_Src tmp("my_func");
|
|
//
|
|
// To adopt a buffer returned by atr_get/atr_pget (caller-owned):
|
|
// LBuf_Adopt buf(atr_pget(thing, A_DESC, &aowner, &aflags));
|
|
//
|
|
class LBuf {
|
|
UTF8 *m_buf;
|
|
const UTF8 *m_file;
|
|
int m_line;
|
|
|
|
struct adopt_tag {};
|
|
|
|
LBuf(UTF8 *buf, const UTF8 *file, int line, adopt_tag)
|
|
: m_buf(buf), m_file(file), m_line(line) {}
|
|
|
|
public:
|
|
LBuf(const UTF8 *tag, const UTF8 *file, int line)
|
|
: m_buf(pool_alloc_lbuf(tag, file, line)),
|
|
m_file(file), m_line(line) {}
|
|
|
|
~LBuf() { if (m_buf) pool_free_lbuf(m_buf, m_file, m_line); }
|
|
|
|
LBuf(const LBuf &) = delete;
|
|
LBuf &operator=(const LBuf &) = delete;
|
|
|
|
LBuf(LBuf &&other) noexcept
|
|
: m_buf(other.m_buf), m_file(other.m_file), m_line(other.m_line)
|
|
{ other.m_buf = nullptr; }
|
|
|
|
LBuf &operator=(LBuf &&other) noexcept {
|
|
if (this != &other) {
|
|
if (m_buf) pool_free_lbuf(m_buf, m_file, m_line);
|
|
m_buf = other.m_buf;
|
|
m_file = other.m_file;
|
|
m_line = other.m_line;
|
|
other.m_buf = nullptr;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
static LBuf adopt(UTF8 *buf, const UTF8 *file, int line) {
|
|
return LBuf(buf, file, line, adopt_tag{});
|
|
}
|
|
|
|
// Relinquish ownership and return the raw buffer; the caller becomes
|
|
// responsible for freeing it (e.g. a producer that returns an lbuf for the
|
|
// caller to adopt). After release() the LBuf is empty and frees nothing.
|
|
UTF8 *release() { UTF8 *b = m_buf; m_buf = nullptr; return b; }
|
|
|
|
// Free the held buffer now and become empty (idempotent).
|
|
void reset() {
|
|
if (m_buf) pool_free_lbuf(m_buf, m_file, m_line);
|
|
m_buf = nullptr;
|
|
}
|
|
|
|
UTF8 *get() { return m_buf; }
|
|
const UTF8 *get() const { return m_buf; }
|
|
operator UTF8 *() { return m_buf; }
|
|
operator const UTF8 *() const { return m_buf; }
|
|
UTF8 &operator[](size_t i) { return m_buf[i]; }
|
|
const UTF8 &operator[](size_t i) const { return m_buf[i]; }
|
|
};
|
|
|
|
#define LBuf_Src(tag) LBuf(T(tag), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define LBuf_Adopt(expr) LBuf::adopt((expr), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
|
|
//! \class LBufPtr
|
|
// Owning, nullable, movable handle for a pool lbuf.
|
|
//
|
|
// LBuf always allocates in its constructor and is the right tool for a scoped
|
|
// scratch buffer. LBufPtr differs in being default-constructible to empty,
|
|
// reseatable, and able to release() its ownership. That makes it suitable for:
|
|
// - conditional ownership (own a buffer on some paths, borrow on others),
|
|
// - holding an lbuf in a struct/queue entry across function boundaries,
|
|
// - a producer that hands the buffer back to its caller (construct, fill,
|
|
// release() at the single success return — early/error returns free it).
|
|
//
|
|
// Usage:
|
|
// LBufPtr buf; // empty
|
|
// buf = LBufPtr_Src("producer"); // allocate
|
|
// ... fill buf ...
|
|
// return buf.release(); // caller now owns it
|
|
//
|
|
// LBufPtr held = LBufPtr_Adopt(atr_pget(...)); // adopt caller-owned buffer
|
|
//
|
|
class LBufPtr {
|
|
UTF8 *m_buf;
|
|
const UTF8 *m_file;
|
|
int m_line;
|
|
|
|
struct adopt_tag {};
|
|
|
|
LBufPtr(UTF8 *buf, const UTF8 *file, int line, adopt_tag)
|
|
: m_buf(buf), m_file(file), m_line(line) {}
|
|
|
|
public:
|
|
LBufPtr() : m_buf(nullptr), m_file(nullptr), m_line(0) {}
|
|
|
|
LBufPtr(const UTF8 *tag, const UTF8 *file, int line)
|
|
: m_buf(pool_alloc_lbuf(tag, file, line)),
|
|
m_file(file), m_line(line) {}
|
|
|
|
~LBufPtr() { if (m_buf) pool_free_lbuf(m_buf, m_file, m_line); }
|
|
|
|
LBufPtr(const LBufPtr &) = delete;
|
|
LBufPtr &operator=(const LBufPtr &) = delete;
|
|
|
|
LBufPtr(LBufPtr &&other) noexcept
|
|
: m_buf(other.m_buf), m_file(other.m_file), m_line(other.m_line)
|
|
{ other.m_buf = nullptr; }
|
|
|
|
LBufPtr &operator=(LBufPtr &&other) noexcept {
|
|
if (this != &other) {
|
|
if (m_buf) pool_free_lbuf(m_buf, m_file, m_line);
|
|
m_buf = other.m_buf;
|
|
m_file = other.m_file;
|
|
m_line = other.m_line;
|
|
other.m_buf = nullptr;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
static LBufPtr adopt(UTF8 *buf, const UTF8 *file, int line) {
|
|
return LBufPtr(buf, file, line, adopt_tag{});
|
|
}
|
|
|
|
// Relinquish ownership; caller becomes responsible for freeing.
|
|
UTF8 *release() { UTF8 *b = m_buf; m_buf = nullptr; return b; }
|
|
|
|
// Free the held buffer now and become empty (idempotent).
|
|
void reset() {
|
|
if (m_buf) pool_free_lbuf(m_buf, m_file, m_line);
|
|
m_buf = nullptr;
|
|
}
|
|
|
|
UTF8 *get() { return m_buf; }
|
|
const UTF8 *get() const { return m_buf; }
|
|
operator UTF8 *() { return m_buf; }
|
|
operator const UTF8 *() const { return m_buf; }
|
|
UTF8 &operator[](size_t i) { return m_buf[i]; }
|
|
const UTF8 &operator[](size_t i) const { return m_buf[i]; }
|
|
explicit operator bool() const { return m_buf != nullptr; }
|
|
};
|
|
|
|
#define LBufPtr_Src(tag) LBufPtr(T(tag), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
#define LBufPtr_Adopt(expr) LBufPtr::adopt((expr), reinterpret_cast<const UTF8 *>(__FILE__), __LINE__)
|
|
|
|
#include <memory>
|
|
|
|
//! \struct RegBuffer
|
|
// Shared buffer that packs multiple register values into a single
|
|
// LBUF_SIZE block. Reference-counted via std::shared_ptr.
|
|
struct RegBuffer
|
|
{
|
|
UTF8 data[LBUF_SIZE];
|
|
size_t used;
|
|
|
|
RegBuffer() : used(0) { memset(data, 0, sizeof(data)); }
|
|
};
|
|
|
|
//! \struct reg_ref
|
|
// A register value — a view into a shared RegBuffer.
|
|
// Allocated with new/delete (not pool-allocated).
|
|
// The refcount tracks how many pointers reference this reg_ref.
|
|
// The shared_ptr<RegBuffer> automatically manages the underlying buffer.
|
|
struct reg_ref
|
|
{
|
|
int refcount;
|
|
std::shared_ptr<RegBuffer> buf;
|
|
size_t reg_len;
|
|
UTF8 *reg_ptr;
|
|
|
|
reg_ref() : refcount(0), reg_len(0), reg_ptr(nullptr) {}
|
|
};
|
|
|
|
#endif //!ALLOC_H
|