Commit graph

27 commits

Author SHA1 Message Date
Stephen Dennis
fb6362eb61 fix(comsys/mail): activate modules and sync softcode with module store (#1191)
Root cause: comsys_mod/mail_mod lacked mux_CanUnloadNow, so ModuleLoad
never set bLoaded and CreateInstance always returned CLASSNOTAVAILABLE —
commands and softcode both used the engine path by accident.

With modules actually loaded:
- Bump module revision on every write-through; softcode reloads engine
  maps from SQLite when the revision advances (select_channel, mail_fetch,
  MailList, etc.).
- CComsysStorage::SyncChannel sets has_comsys so softcode reload works.
- Channel Sync uses ON CONFLICT DO UPDATE so updates do not CASCADE-wipe
  channel_users/player_channels.
- mailsend() goes through IMailControl::SoftcodeSend; bodies and
  mail_db_top are write-through for softcode mail_* fields.
2026-07-25 08:33:42 -06:00
Stephen Dennis
9e3d559096 fix(sqlitedb): bump CURRENT_SCHEMA_VERSION to 13 so v12/v13 migrations run (#1073)
The migration gate was never bumped when the v12 (max_func_depth, #1002)
and v13 (n_func_calls) migrations were added.  An existing database at
schema 11 or 12 early-returned out of MigrateSchema, skipped the pending
ALTERs, and then PrepareStatements failed on the missing code_cache
columns: the server dies at boot with 'Couldn't open storage backend.'

Fresh databases run the full chain from version 0 and never see it,
which is why the fresh-db smoke stayed green.  Verified on Windows by
warm-starting a 2.14.0.9-era v11 netmux.sqlite: pre-fix boot fails as
above; post-fix both migrations run and the game loads.

Closes #1073

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:42:41 -06:00
Stephen Dennis
028f51bef2 feat(jit): persist n_func_calls in code_cache (schema v13)
Store the static function-invocation watermark alongside max_func_depth
so @restart reconstructs it from SQLite instead of leaving n_func_calls
at 0 until each expression recompiles into the memory LRU. Pre-v13 rows
default to 0 (watermark skipped until recompile), matching the
max_func_depth progressive path.
2026-07-23 20:00:47 -06:00
Stephen Dennis
7dbe19e775 jit: static-depth watermark for function_recursion_limit (#1002)
Resolves the #1002 design question as a semantic contract, per the
watermark sketch: compiled code flattens function nests and maintains
no func_nest_lev, so a JIT run that could reach the limit now declines
to the AST, which reproduces "#-1 FUNCTION RECURSION LIMIT EXCEEDED"
exactly.

- compiled_program.max_func_depth: maximum AST_FUNCCALL nesting of the
  parse tree, computed at compile_expression and persisted through the
  write queue into the SQLite code cache (schema v12 adds the column;
  pre-v12 rows read 0 but are invalidated by the blob-version stamp
  regardless).
- jit_eval declines when live func_nest_lev + max_func_depth >=
  mudconf.func_nest_lim — the LIVE values, so nested entry contexts
  count and @admin limit changes apply to cached programs.  The check
  precedes the folded-result shortcut (folding pre-computes the nest
  and must obey the same contract).  run_cached_program repeats the
  check for callers that bypass jit_eval (rvbench).  New bail_depth
  jitstats counter.
- Documented boundary: ECALL-dispatched callees do not bump
  func_nest_lev, so cross-ECALL nesting carries a one-level skew;
  probe-writing softcode should not assert at exactly limit-1 across
  a u() boundary.

jit_parity_fn.mux TC019 locks it: depths 4 and 6 at limit 6 (the
unambiguous zones; depth 5 sits in the skew zone and is deliberately
unasserted), with the limit juggling and u() probes run at Makesmoke
bake time as #1 — @admin is God-only and a runtime-triggered attribute
executes as the object (the nested_depth.mux pattern; the first cut
tried runtime @admin and the assertion ran against the unchanged
limit).

Verified: nest4 ok4 via JIT, nest6 declines with bail_depth=1 and
returns the exact AST error, restore honored by cached programs.
Smoke 1319/1319 both toggles; oracle 9/9; sweeps standard and
utf8+longreg(SEED=7) 400/0 LOGIC; GANL 14/0; netaddr 33/0.

Closes #1002.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:55:28 -06:00
Stephen Dennis
a056f67d0a 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>
2026-07-07 19:52:53 -06:00
Stephen Dennis
ecae4df9de dbload: mistake-proof the SQLite import path (#766)
Migrating an existing game to the SQLite backend had two silent traps that
both surfaced as "old passwords/characters don't work":

  1. dbconvert resolves <basename>.sqlite relative to cwd, but the server
     reads data/<name>.sqlite. Running db_load from the wrong directory put
     the database where the server never looks, so old characters appeared
     to vanish while a fresh login still worked against the stock db.
  2. A populated netmux.sqlite silently shadows the netmux.db flatfile at
     boot, so dropping in an old flatfile did nothing.

Changes:
  - db_load/db_unload are now cwd-independent: the .sqlite always lands in
    the game's data/ dir (next to the script), file args are resolved
    against the caller's dir, and the scripts echo the absolute path. Arg
    handling is space-safe (set --) and POSIX sh.
  - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath).
  - The "would overwrite" guard now names the file and gives two ways
    forward; a new -f/force option lets a load replace an existing db (the
    load already clears attributes/objects/attr-names cleanly).
  - The server logs a line when it warm-starts from SQLite and the flatfile
    was not consulted, making the precedence visible.
  - New docs/importing-a-game.md documents the two-file model and import
    steps.

Verified: db_load from an unrelated dir lands the .sqlite in data/; the
guard refuses without -f and replaces with -f; db_unload round-trip is
byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures
(TC001/TC009 pre-existing on master).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:27:43 -05:00
Stephen Dennis
6aceec7627 engine/sqlite: null-guard column reads, fix reset/rollback/checkpoint paths
Hardening of the SQLite backend against OOM-NULL column results and against
error/busy paths that were dropped or left statements in a bad state.

NULL column guards. sqlite3_column_blob()/_text() may return NULL with bytes > 0
under OOM; the previous code fed those straight into memcpy or a consumer that
dereferences them.
  - GetAttribute (#727): NULL blob with blobLen > 0 is now treated as "not
    found"; the memcpy is also skipped for a legitimate zero-length attribute
    (NULL blob, blobLen == 0) which remains valid.
  - GetAllAttributes (#728) and GetBuiltinAttributes (same pattern): skip a row
    whose blob is NULL with len > 0 instead of passing NULL to the callback.
  - LoadAllAttrNames (#732): skip rows with a NULL name column rather than
    handing the callback a NULL C string.

CodeCacheFlush (#729): reset the statement *after* stepping, not before, so an
error return (SQLITE_IOERR, SQLITE_FULL, ...) cannot leave it holding error/lock
state that makes the next use fail spuriously.

RunMigration (#730): capture and log the ROLLBACK result on the migration
failure path. Previously a failing ROLLBACK (busy, I/O error) was discarded with
a NULL errmsg, hiding that the connection was left in an indeterminate
transaction state. (The referenced abort_dump_no_restart() does not exist in
this module; the migration already aborts via its false return.)

Checkpoint (#731): retry sqlite3_wal_checkpoint_v2() on SQLITE_BUSY with short
backoff (up to 5 attempts, 10-40 ms). Previously a single busy return made @dump
report success while the WAL kept growing; busy is rare in the single-threaded
engine but possible under dbconvert -m. A persistent failure is now logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:03:35 -06:00
Stephen Dennis
ac0b92933a Validate SQLite code cache blobs 2026-04-04 16:34:54 -06:00
Stephen Dennis
158541ff78 Reset SQLite statement on code cache failure 2026-04-04 16:30:44 -06:00
Stephen Dennis
d37d825366 Fix duplicate SQLite migration logging 2026-04-04 16:28:48 -06:00
Stephen Dennis
1ba6350faa Compact SQLite JIT cache images 2026-04-04 10:01:45 -06:00
Stephen Dennis
977c4087b6 Move db/ to tests/db/, add tests/libmux/ unit test harness
Reorganize standalone test harnesses under tests/:
- tests/db/: SQLite/storage backend tests (moved from db/)
  - Fix missing <ctime> include in sqlitedb.cpp
  - Add stubs.cpp for attr_mod_count symbols
  - Update Makefile paths for new location
- tests/libmux/: New unit test harness linking against libmux.so
  - 29 tests covering mux_atol, mux_atof, mux_i64toa, safe buffer
    writing, StringClone, mux_stricmp, mux_strupr/strlwr,
    trim_spaces, mux_strncpy
- tests/color_ops/: Unchanged (already in tests/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:08:41 -06:00
Stephen Dennis
a348e27f33 Implement Phase 1 routing: static unconditional next-hop tables
Add route() softcode function with BFS-based shortest-path routing
over rooms marked NAVIGABLE. The routing table stores only the next-hop
exit for each (source, dest) pair, compressed via diagonal elimination,
adjacent marking, and row redundancy. Lazy rebuild on generation-counter
mismatch triggered by topology changes (@dig, @destroy, @link, @open,
@unlink) and NAVIGABLE flag changes. SQLite schema v10 adds route_nodes,
route_table, route_meta tables for future persistence phases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 03:18:37 -06:00
Stephen Dennis
512790312b Disable WAL auto-checkpoint for code cache writes
The code cache is a cache — structural integrity matters but
durability per-write does not. Disabling auto-checkpoint eliminates
fsync from the hot path. The WAL accumulates writes during normal
operation; explicit checkpoint at @dump and shutdown handles
persistence.

Smoke suite: 2s → 1s (50% reduction in kernel time).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:49:29 -06:00
Stephen Dennis
ac58b54c32 Revive @dbclean: purge orphaned vattr names and refresh SQLite stats
@dbclean now finds user-defined attribute names (attrnum >= 256) that
no object references, removes them from both SQLite and the in-memory
vattr maps, then runs ANALYZE to refresh query planner statistics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 05:40:48 -06:00
Stephen Dennis
8529bb68f2 Add vlimit config parameter for per-object attribute count limit
Default 1000, 0 = unlimited. Wizard-owned objects are exempt.
Enforced in atr_add_raw_LEN with a fast cache_get existence check
(updates are free) and SQLite COUNT(*) for new attributes only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 05:09:02 -06:00
Stephen Dennis
b7faec4e12 Add Tier 3 u() inlining in hir_lower_funccall (Step 6/7)
When the JIT compiler sees u(obj/attr) or ulocal(obj/attr) with a
constant first argument, it resolves the attr at compile time, parses
the body, and lowers it inline into the caller's HIR.

Three tiers cooperating:
  Tier 1: resolves obj/attr at compile time (parse_attrib + atr_pget)
  Tier 2: body's pure functions run as guest Tier 2 calls
  Tier 3: orchestrates inlining with helper ECALLs

Correctness requirements (all addressed):
  - Runtime permission: _CHECK_U_PERM → BRC fallback to host fun_u
  - CARGS scoping: _SAVE_CARGS / _WRITE_CARG / _SET_NCARGS / _RESTORE
  - ULOCAL qregs: _SAVE_QREGS / _RESTORE_QREGS
  - Cache staleness: deps vector with per-attr mod_count
  - Inline depth limit: MAX_INLINE_DEPTH = 3

Also fixes schema bootstrap: v7 migration CREATE TABLE reverted to
original (no deps_blob), v9 ALTER TABLE handles all paths.

Known issue: certain complex inlined bodies (e.g., switch() with
multiple branches) cause compilation hangs under benchmark().
Single invocations work correctly.  Needs investigation.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:52:08 -06:00
Stephen Dennis
1ef0d748d2 Fix code_cache schema: include deps_blob in all CREATE TABLE paths
Fresh databases and v7 migration both created code_cache without
deps_blob, but the v9 prepared statements require it.  Added
deps_blob to both the base CreateSchema and v7 migration CREATE
TABLE definitions so all paths land on the same schema.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:20:14 -06:00
Stephen Dennis
2dccc7d347 Add dependency vector to compiled_program + SQLite schema (Step 4/7)
compiled_program now carries a deps vector of {obj, attr_num,
mod_count} recording which attrs were inlined at compile time.

Schema v9: adds deps_blob BLOB column to code_cache table.
Dependencies are serialized as a packed binary array (12 bytes
per entry) for minimal storage and fast deserialization.

store_to_sqlite_cache persists deps alongside the compiled blob.
reconstruct_from_cache restores them on SQLite cache hit.
CURRENT_SCHEMA_VERSION bumped to 9.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:07:39 -06:00
Stephen Dennis
7d7ea2ea79 Fix bulk deletion mod_count: monotonic increment, no sentinel
The UINT32_MAX sentinel wrapped to 0 on next write, breaking
monotonicity. Replace with:

- attr_mod_count_invalidate_object() now seeds all attr counters
  from SQLite BEFORE deletion (via GetAllAttrModCounts), then
  increments each by 1. Counters are strictly monotonic — no
  sentinel, no wrapping, no reuse across delete/recreate cycles.

- Call sites in object.cpp moved BEFORE DelAllAttributes so the
  SQLite rows are still readable when seeding.

- ClearAttributes() clears the in-memory map entirely (correct
  for full database reload where JIT cache is also cleared).

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:25:50 -06:00
Stephen Dennis
0f3d233616 Fix mod_count failure-path divergence and bulk deletion bypass
Two bugs from code review:

1. attr_mod_count_inc ran before cache_put/cache_del, so failed
   writes bumped the in-memory counter without changing SQLite.
   Fix: increment AFTER successful storage. PutAttribute reads
   the current counter and passes counter+1 to the upsert
   independently.

2. DelAllAttributes and ClearAttributes bypassed mod_count tracking.
   Fix: attr_mod_count_invalidate_object() sets all entries for a
   destroyed object to UINT32_MAX (always stale). ClearAttributes()
   calls attr_mod_count_invalidate_all() to clear the entire map.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:20:16 -06:00
Stephen Dennis
acae612fea Fix mod_count divergence and delete/recreate reuse
Two bugs from code review:

1. In-memory counter started at 0 regardless of persisted value.
   After restart, first write to an attr with mod_count=17 in SQLite
   would set in-memory to 1 while SQLite advanced to 18.
   Fix: attr_mod_count_inc() seeds from SQLite on first access.

2. Delete + recreate reused mod_count=1, making stale cache entries
   look fresh.  Fix: increment in-memory counter BEFORE deletion
   (survives the SQLite row removal), and PutAttribute uses
   MAX(excluded.mod_count, mod_count+1) so the SQLite value is
   always >= the in-memory value.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:11:02 -06:00
Stephen Dennis
2be7d9a1b5 Add per-attr mod_count for JIT cache invalidation (Step 1/7)
Foundation for Tier 3 u() inlining cache staleness detection.
Each attribute now tracks a modification counter that increments
on every atr_add/atr_clr.  The JIT compiler will record mod_count
at compile time and check for staleness at runtime.

Schema changes:
- Bump to v8: ALTER TABLE attributes ADD COLUMN mod_count
- PutAttribute uses INSERT...ON CONFLICT DO UPDATE to atomically
  increment mod_count on each write
- New GetAttrModCount() method for SQLite lookup

In-memory tracking:
- attr_mod_count_inc() incremented from atr_add_raw_LEN / atr_clr
- attr_mod_count_get() reads from in-memory map with SQLite fallback
- Survives LRU cache eviction (separate map, not in AttrCacheEntry)

Also fixes CURRENT_SCHEMA_VERSION (was 6, now 8).

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:02:46 -06:00
Stephen Dennis
d9ab0546f7 JIT: harden compile cache keys and extract SHA1 wrapper 2026-03-15 02:18:13 -06:00
Stephen Dennis
e02b7566a5 Stage 5: SQLite code cache — persist compiled programs across restarts
Add code_cache table (schema v7) to persist compiled softcode.
On compile-cache miss, check SQLite before compiling from scratch.
After successful compilation, store to SQLite for future restarts.

Cache keyed by expression text + blob version (size:func_count).
Blob version mismatch → stale entry skipped (tier2 addresses change).
Memory blob stores code+strings+fargs (0-32KB per entry).
Reconstructed programs get tier2 blob reinstalled at load time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 22:12:11 -06:00
Stephen Dennis
a455de9a96 Add connection logging: SQLite connlog table, connlog()/addrlog() functions
SQLite schema v6 adds connlog table with indexes on player, connect_time,
and ipaddr. AnnounceConnect inserts a row; AnnounceDisconnect updates with
disconnect time and reason. connlog(player[,limit]) returns pipe-delimited
records; addrlog(pattern[,limit]) does SQL LIKE matching (wizard-only).

COM interface updated: IPlayerSession::AnnounceConnect gains int64_t*
pConnlogId out-param, AnnounceDisconnect gains int64_t connlogId in-param.
DESC struct carries connlog_id through the session lifetime.

Also fixes isalpha_fn.mux test hashes after DFA table rebuild.

444/444 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:00:30 -06:00
Stephen Dennis
4ff1398de1 Restructure mux/ directory: component-based layout with proper build root
Move from flat mux/src/ layout to clean component hierarchy:
- mux/ is now the autoconf/automake build root (configure.ac lives here)
- mux/include/ — shared headers used by multiple components
- mux/lib/ — libmux.so (core utilities, no game state)
- mux/src/ — netmux driver only (thin networking shell)
- mux/modules/engine/ — engine.so (game logic)
- mux/modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ — external modules
- mux/ganl/ — GANL networking library
- mux/sqlite/ — SQLite amalgamation (builds libsqlite3.a)
- mux/announce/ — announce tool (was mux/src/tools/)

Build changes:
- SUBDIRS ordering: ganl sqlite lib src modules announce
- libmux.so gets -Wl,-soname,libmux.so; netmux links via -L -lmux
- engine.so links libsqlite3.a and libmux.so with -Wl,--no-undefined
- RPATH uses $ORIGIN for portable .so resolution
- Install hooks use absolute paths for game/bin symlinks

Bug fixes:
- engine.so mux_Register() now passes nullptr to mux_RegisterClassObjects
  (matches all other modules; libmux already has the factory via dlsym)
- DbConvert() now calls pcache_init() before db_write, fixing a latent
  crash (free(): invalid pointer) when exporting from SQLite databases

411/411 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:38:37 -06:00
Renamed from mux/src/sqlitedb.cpp (Browse further)