mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
harden: fix five memory-safety/DoS bugs in database load paths
Audit of the DB load paths (flatfile reader, mail/malias loader, SQLite attribute bulk-load) under the malicious/corrupt-database threat model -- the same class as #834-843. Five distinct bugs, all in code the June hardening (#806/#808/#841/#843) did not reach; each is reachable only from crafted or tampered database content, not from normal gameplay. Verified by full rebuild + smoke suite (1264/1264, 0 crashes), which exercises flatfile export/import and SQLite attribute reads end to end. 1. getstring_noalloc: static buffer overflow (lib/dbutil.cpp) The escaped-string reader mis-accumulated its output-byte count: `nOutput = pOutput - p` overwrote the count from prior escape emits instead of adding to it. On the multi-fgets refill path this under-decrements nBufferLeft, so the `nBufferLeft <= 0` guard never trips and the next fgets writes past the 2*LBUF_SIZE+20 static buffer. Legitimate attributes (<= LBUF) never take the refill path, so this only fires on a crafted quoted string > ~64KB. Fix: accumulate (+=). 2. make_numlist / malias_read: stack buffer overflow (modules/engine/mail.cpp) malias_read read the recipient count (numrecep) straight from the file and pushed that many dbrefs into m->list with no cap, making m->list.size() attacker-controlled. make_numlist then copied all of m->list into the fixed stack array aRecip[(LBUF_SIZE+1)/2] with no bound on nRecip -- a crafted mail.db with numrecep > 16384 overflows the stack (with attacker-chosen dbrefs) the next time any player mails the alias. Fix: clamp numrecep to (LBUF_SIZE+1)/2 at load (also bounds the reserve() that could otherwise exhaust memory on an INT_MAX count), and defensively bound the copy loop in make_numlist. 3. SQLite bulk-load attribute value: heap buffer overflow (modules/engine/sqlitedb.cpp) GetAllAttributes/GetBuiltinAttributes passed the raw column blob length to the cache with no clamp, unlike the write path (cache_put) and the standalone read path (GetAttribute), both of which clamp to LBUF_SIZE. A value blob written directly into the SQLite file therefore flows unclamped to atr_get_str_LEN's `memcpy(s, buff, (*pLen)+1)` into a fixed LBUF_SIZE buffer, overflowing the heap on first read of the attribute (Name/look/examine/get). Fix: clamp len to LBUF_SIZE in both bulk-load functions, mirroring the existing clamps. 4. get_list: infinite loop + unbounded log on truncated flatfile (modules/engine/db_rw.cpp) get_list had no EOF case: at end-of-file getc() returns EOF, falls to default, and calls getstring_noalloc(), which makes no progress at EOF (ungetc(EOF) is a no-op, fgets returns NULL). The for(;;) then spins forever, pegging a core and emitting log lines. Trigger: a flatfile whose last object's attribute list is truncated before its '<' terminator. Fix: add a case EOF that aborts the load. 5. getboolexp1: BOOLEXP subtree leak on malformed v2 lock (modules/engine/db_rw.cpp) Three error paths in the v2 lock parser returned TRUE_BOOLEXP without freeing the partially built node/subtree (bad connective, missing ')', EOF mid lock-string). Bounded and single-shot (db_read aborts the whole load on corruption; nesting capped at 1024), but still a leak on crafted input. Fix: free the appropriate node/subtree at each site, matching the partial-construction state. Also traced and dropped a sixth candidate (SQLite attribute owner read unvalidated): the attribute owner is never used to index db[] -- only compared, read for its flags, or re-stored -- so a corrupt owner is harmless. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
94d278f553
commit
a056f67d0a
4 changed files with 60 additions and 2 deletions
|
|
@ -224,7 +224,7 @@ void *getstring_noalloc(FILE *f, bool new_strings, size_t *pnBuffer)
|
|||
*pOutput++ = ch;
|
||||
ch = *pInput++;
|
||||
} while (decode_table[static_cast<unsigned char>(ch)] == 0);
|
||||
nOutput = pOutput - p;
|
||||
nOutput += pOutput - p;
|
||||
}
|
||||
}
|
||||
int iAction = action_table[iState][decode_table[static_cast<unsigned char>(ch)]];
|
||||
|
|
|
|||
|
|
@ -293,6 +293,12 @@ static BOOLEXP *getboolexp1(FILE *f, int depth)
|
|||
break;
|
||||
|
||||
default:
|
||||
// Bad connective: b->sub1 (built above) and the node leak
|
||||
// otherwise. b->type is not yet set, so free sub1 explicitly
|
||||
// rather than via free_boolexp(b).
|
||||
//
|
||||
free_boolexp(b->sub1);
|
||||
free_bool(b);
|
||||
goto error;
|
||||
}
|
||||
b->sub2 = getboolexp1(f, depth + 1);
|
||||
|
|
@ -304,6 +310,10 @@ static BOOLEXP *getboolexp1(FILE *f, int depth)
|
|||
}
|
||||
if (')' != d)
|
||||
{
|
||||
// Missing close paren: b->type and its subtree(s) are fully set
|
||||
// here, so free_boolexp() unwinds them correctly.
|
||||
//
|
||||
free_boolexp(b);
|
||||
goto error;
|
||||
}
|
||||
return b;
|
||||
|
|
@ -395,6 +405,10 @@ static BOOLEXP *getboolexp1(FILE *f, int depth)
|
|||
|
||||
if (EOF == c)
|
||||
{
|
||||
// EOF mid lock-string: b->sub1 not yet allocated (StringClone
|
||||
// below is unreached), so free only the node.
|
||||
//
|
||||
free_bool(b);
|
||||
goto error;
|
||||
}
|
||||
*s = '\0';
|
||||
|
|
@ -515,6 +529,16 @@ static bool get_list(FILE *f, dbref i)
|
|||
}
|
||||
return true;
|
||||
|
||||
case EOF:
|
||||
|
||||
// Truncated flatfile: the attribute list was cut off before its
|
||||
// '<' terminator. Abort the load rather than spinning forever —
|
||||
// getstring_noalloc() makes no progress at EOF, so the default
|
||||
// case below would loop indefinitely emitting log lines.
|
||||
//
|
||||
Log.tinyprintf(T("Unexpected EOF when getting attributes on object %d" ENDLINE), i);
|
||||
return false;
|
||||
|
||||
default:
|
||||
Log.tinyprintf(T("Bad character \xE2\x80\x98%c\xE2\x80\x99 when getting attributes on object %d" ENDLINE), c, i);
|
||||
|
||||
|
|
|
|||
|
|
@ -2311,7 +2311,10 @@ static UTF8 *make_numlist(dbref player, UTF8 *arg, bool bBlind)
|
|||
tprintf(T("MAIL: \xE2\x80\x98%s\xE2\x80\x99 is a badly-formed alias."), head));
|
||||
return nullptr;
|
||||
}
|
||||
for (size_t i = 0; i < m->list.size(); i++)
|
||||
for (size_t i = 0;
|
||||
i < m->list.size()
|
||||
&& nRecip < static_cast<int>(sizeof(aRecip)/sizeof(aRecip[0]));
|
||||
i++)
|
||||
{
|
||||
aRecip[nRecip++] = m->list[i];
|
||||
}
|
||||
|
|
@ -3871,6 +3874,17 @@ static void malias_read(FILE *fp, bool bConvert)
|
|||
|
||||
if (numrecep > 0)
|
||||
{
|
||||
// Clamp a hostile/corrupt recipient count. A legitimate malias is
|
||||
// bounded by the LBUF command string that creates it, which is why
|
||||
// make_numlist's aRecip[] is sized (LBUF_SIZE+1)/2. Without this,
|
||||
// m->list grows unbounded from file content and later overflows
|
||||
// aRecip[] in make_numlist; reserve() below could also exhaust
|
||||
// memory on an INT_MAX count.
|
||||
//
|
||||
if (numrecep > (LBUF_SIZE+1)/2)
|
||||
{
|
||||
numrecep = (LBUF_SIZE+1)/2;
|
||||
}
|
||||
m->list.reserve(numrecep);
|
||||
for (j = 0; j < numrecep; j++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1601,6 +1601,16 @@ bool CSQLiteDB::GetAllAttributes(dbref obj, AttrCallback cb)
|
|||
dbref owner = static_cast<dbref>(sqlite3_column_int(m_stmtAttrGetObj, 2));
|
||||
int flags = sqlite3_column_int(m_stmtAttrGetObj, 3);
|
||||
|
||||
// Clamp a tampered/corrupt value blob to LBUF_SIZE, mirroring the
|
||||
// write-path clamp (cache_put) and the standalone read clamp
|
||||
// (GetAttribute). The consumer (atr_get_str_LEN) memcpy's this length
|
||||
// into a fixed LBUF_SIZE buffer; an oversized blob would overflow it.
|
||||
//
|
||||
if (len > LBUF_SIZE)
|
||||
{
|
||||
len = LBUF_SIZE;
|
||||
}
|
||||
|
||||
// sqlite3_column_blob() returns NULL with bytes > 0 only under OOM; skip
|
||||
// such a row rather than handing the consumer a NULL it will dereference.
|
||||
if (NULL == value && 0 != len)
|
||||
|
|
@ -1640,6 +1650,16 @@ bool CSQLiteDB::GetBuiltinAttributes(dbref obj, AttrCallback cb)
|
|||
dbref owner = static_cast<dbref>(sqlite3_column_int(m_stmtAttrGetBuiltin, 2));
|
||||
int flags = sqlite3_column_int(m_stmtAttrGetBuiltin, 3);
|
||||
|
||||
// Clamp a tampered/corrupt value blob to LBUF_SIZE, mirroring the
|
||||
// write-path clamp (cache_put) and the standalone read clamp
|
||||
// (GetAttribute). The consumer (atr_get_str_LEN) memcpy's this length
|
||||
// into a fixed LBUF_SIZE buffer; an oversized blob would overflow it.
|
||||
//
|
||||
if (len > LBUF_SIZE)
|
||||
{
|
||||
len = LBUF_SIZE;
|
||||
}
|
||||
|
||||
// sqlite3_column_blob() returns NULL with bytes > 0 only under OOM; skip
|
||||
// such a row rather than handing the consumer a NULL it will dereference.
|
||||
if (NULL == value && 0 != len)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue