do_restart() rebuilds its exec argv from mudconf.pid_file and
mudconf.log_dir. Both were read in three places and written in NONE:
predicates.cpp the execl <- read
engine_com.cpp GetConfig -> DRIVER_CONFIG.pid_file <- read
engine_com.cpp GetConfig -> DRIVER_CONFIG.log_dir <- read
There is no `pid_file` or `log_dir` config directive either, so nothing in
netmux.conf could populate them. Both are driver-owned CLI values --
driver.cpp says so outright ("pid_file is driver-owned -- set from CLI or
default") -- and the engine had no way to learn them. So every @restart on
every site exec'd with an empty -p and -e.
Impact is quiet, which is why it lasted: a site running
netmux -p /var/run/mux/netmux.pid
has its pidfile revert to the default netmux.pid in the working directory
after the first restart, and stops maintaining the configured path. The
pid inside the stale file stays correct, because execve preserves it, so
nothing breaks until something else moves. Same for -e.
The fingerprint is visible in ps on any restarted 2.14: `-p` with nothing
after it.
Fix follows #817's precedent exactly -- driver-owned strings the engine
needs, handed over at bridge init:
- g_driver_pid_file / g_driver_log_dir replace two file-static CLI
variables in driver.cpp (pErrorBasename was also a poor name for
something that is a log directory). Globals for the same reason
g_version is one: the CDriverControl bridge is a different TU.
- mux_IDriverControl gains GetInvocationPaths. IID bumped D4D6 -> D4D7;
the comment now records both bumps and why a vtable change needs one.
- conn_bridge_init caches them into mudconf. Safe there because
Startup() runs after LoadGame(), so cf_init() cannot wipe them
afterwards. Pointers are stored, not copied: they are driver-owned
storage that lives for the process, and nothing in the engine frees or
reassigns either field.
- CScriptDriverControl implements it too, returning empty strings.
muxscript writes no pidfile and has no @restart; an invented default
would only be wrong somewhere later.
The argv is now built dynamically and omits an option it has no value for.
-c, -p and -e are all CLI_REQUIRED, so a bare flag makes the successor log
"Option 'x' requires an argument, but none was found." Populating the
fields alone would have left that warning for any site that supplies only
one of the two -- which is what test-scenario showed, since run.sh passes
-p and not -e.
Verified on a live restart, reading the successor's argv (execve replaces
argv and preserves the pid, so this is direct evidence):
boot: ./bin/netmux -c netmux.conf -p custom-name.pid -e logs
restart: netmux -c netmux.conf -p custom-name.pid -e logs
ok - successor kept -p custom-name.pid
ok - successor kept -e logs
ok - no missing-argument warning in the log
Catch-verified by removing only the bridge caching:
restart: netmux -c netmux.conf -p
not ok - successor lost the -p value
not ok - successor lost the -e value
not ok - successor logged a missing-argument warning:
15:Warning: Option '-p' requires an argument, but none was found.
tests/scenario/restart_helpers.py gains the assertion; it already pays for
a restart, so this costs no extra cycle. It skips if the server under test
was not started with -p.
Clean rebuild, since the IDriverControl vtable changed.
make test: 36 targets, 34 passed, 2 skipped (NLS), 0 failed
config: jit=yes stubslave=yes nls=no realitylvls=yes wodrealms=yes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`. Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.
The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.
New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.
The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper). const_cast budget: zero
added.
Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW). delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup. After any signature flip, grep the
old spelling.
Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#2042 made a failed dump visible; it could not make it survivable, because
by the time dump_restart_db() ran the game was already dismantled. This
moves the dump ahead of the teardown so the restart can simply not happen.
Before, do_restart() ran prepare_for_restart(), final_modules(), pcache_sync,
dump_database_internal, SYNC and CLOSE, and only then wrote restart.db. A
failed write was therefore discovered with nothing left to return to, and the
successor exec'd into a game it could not serve: prepare_for_restart()
detaches the listener fds but deliberately leaves them OPEN for the successor
to adopt, so without restart.db the successor collides with its own
inheritance --
[Kqueue:7] Listener detached (fd left open).
[Kqueue:6] bind() failed: Address already in use
-- and exits. @restart on a full disk made the game disappear.
The ordering that makes declining possible: listener detachment is step 6 of
prepare_for_restart(), not step 1. Steps 1 (record listeners) and 2 (close
TLS/WebSocket) are split into their own methods so DumpRestartDb() can run
exactly what the dump needs, leaving the engine up and the listeners attached
when the result is known. prepare_for_restart() still calls both, so there is
one implementation and any other caller is unaffected; both are idempotent so
re-running them costs nothing.
Step 2 must precede the dump, not follow it: a TLS descriptor written into
restart.db is restored by a successor with no TLS state for it (#2032). That
is also why declining is not free, and the messages say so rather than
implying nothing happened -- those sessions are already gone.
This is abstention, not recovery. No retry, no fd reclamation, no heuristic
about whether the failure looked transient. The dump either produced a
complete file or it did not, and the restart proceeds or does not.
Plumbing: dump_restart_db() returns bool through externs.h, the engine shim
in conn_bridge.cpp, and CDriverControl::DumpRestartDb() -- which already
returned MUX_RESULT and always said MUX_S_OK. No interface method was added,
so the module ABI is unchanged. A missing driver control now reads as "cannot
dump" rather than silent success; proceeding would be the exact failure this
reports.
Verified live on macOS/arm64, past the 15s bCanRestart gate:
normal @restart 2 game logs, 725 bytes answered, no restart.db.tmp
dump fails 1 game log, 725 bytes answered, game ALIVE
RST/DUMP : Could not write restart.db (write failed)...
WIZ/RSTRT : Restart CANCELLED ... Requested by Wizard(#1)
neither restart.db nor restart.db.tmp published
Before this change that second row was ConnectionRefusedError and a dead
process.
The new broadcast is an M_() string, so mux/po/tinymux.pot is regenerated and
es/ko/xx merged; xx.po is a complete-policy pseudo-locale and gets its "[xx] "
entry. The catalogue guard caught the omission -- test-nls failed with
"marked in source but absent from .pot" and named the fix.
make test 35 passed / 1 skipped / 0 failed
(jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BAN_LEGACY falls 41 → 23. Convert every ratchet site that can reach
mux_sprintf/mux_snprintf:
nls path assembly, AST bench floats, attrcache sizes, mail folders,
named references, softcode %f (fixed-precision table; no %.*f),
exp3 safe_ltoa, ganl port, websocket 101 handshake.
Left frozen: dbt_test.cpp (22) and dbt_x64_div_harness.c (1) — standalone
test binaries that do not link libmux. Neither is player-facing text.
Maintainer direction: prefer int64_t over long/int for parsed softcode
and attribute integers so LLP64 and LP64 behave the same.
This first slice covers the leftovers called out on the issue:
- quota: store/show/set paths (avoids @quota/all 2^32 writing 0)
- A_CHARGES in set/levels/predicates (RUNOUT vs wrap-to-positive)
- semaphore counts and @ps/queue kick/warp parsing
- IDLETIMEOUT attribute seconds
Bounded API edges (RunTasks(int), optional int *pnum) clamp after the
full-width parse. pot: quota notify formats use %lld.
Residual Phase 3 player/staff prose that still used T() on live paths:
connect/reconnect/disconnect room and monitor lines, [Suspect] flags,
GAME: restart/shutdown/signal/log-full, dual-path channel boot, and
@email hostname resolution. pot + xx complete + ko msgmerge append;
tools/merge_nls_markings.py for Windows agents without xgettext
(canonical pot remains make -C mux/po pot on Linux).
Finish the predicates.cpp notify pass: @program cleared and the
reference-system raw_notify messages left after the earlier predicates
slice (~9 sites). Leave tprintf formats as T(). Half-mark pass clean.
Regenerate pot/xx without fuzzy entries.
Next notify slice while wiz/cque is in review: permission/lock-style
messages in predicates.cpp and command-processing chatter in
command.cpp (~45 sites). Leave tprintf formats as T(). Half-mark pass
clean. Regenerate pot/xx without fuzzy entries.
Replace ~900 typographic \xE2\x80\x.. escapes (curly quotes, en dashes)
and a few \xE2\x80\230 octal workarounds with real UTF-8 in message
strings under mux/modules and mux/src. Leave stringutil and convert
charset mapping tables as explicit byte sequences.
Mechanical hygiene under the opt-in M_() design: replace T("#-1…") and
T("#-2…") with S_() so softcode ABI tokens are obvious in source and
cannot enter a player catalog. ~400 call sites across engine, exp3,
mail, and driver. Assembled/library-spliced diagnostics (plan §4.2)
are unchanged where they are not a single literal.
add_quota passed mux_atoi64's int64_t straight into mux_ltoa, which takes
long. A no-op on LP64; on LLP64 the high word was simply dropped.
Measured on Win64, quotas enabled, one object refunding one quota:
RQUOTA 8589934592 -> 1 (should be 8589934593)
RQUOTA 4294967296 -> 1 (should be 4294967297)
Both lose the entire high word, and both are correct on Linux, so the
same database gives different quota values depending on the platform that
last touched it.
Clamped rather than widened, which is the decision the issue asked to have
recorded. The quota domain is 32-bit by construction everywhere else:
pay_quota, mung_quotas and do_quota all hold it in int, destroy_obj's
refund is int, and both writers format into UTF8[I32BUF_SIZE]. Widening
add_quota alone would make it the only writer able to store a value every
other reader still truncates, turning a visible error into silent drift.
The stored attribute is only text, and mux_atoi64 parses whatever is in
it, so the clamp is where the domain gets enforced.
Two helpers: quota_from_attr reads A_RQUOTA into the int the rest of the
system expects, and quota_add sums without overflowing (signed overflow
is UB, and INT32_MAX + refund was reachable).
pay_quota is fixed the same way. It had "int quota = mux_atoi64(...)",
which truncates on *every* platform, and I hit it while reproducing this:
a stored 8589934592 silently became -1 there before add_quota ever ran.
That is beyond the issue's literal scope, but it is the same defect in the
same code path, and leaving it means the clamp only half holds. Happy to
split it out if preferred.
After the fix, on Win64:
8589934592 -> 2147483647 clamped, deliberate, platform-independent
4294967296 -> 2147483647
42 -> 43 ordinary values unaffected
-3 -> -2 negatives still work
2147483647 -> 2147483647 saturates instead of overflowing
Because it clamps, behaviour is now identical on LP64 and LLP64.
No smoke test, deliberately. I wrote one and it broke four other cases:
A_RQUOTA is AF_GOD so only #1 can plant the value, but inside the harness
"me" is the test object, so the set silently no-ops; and reaching
add_quota needs mudconf.quotas plus a nonzero thing_quota plus a @dbck,
all of which are server-wide. quotas=1 made permission_paths' object
creation fail and the @dbck perturbed the dolist queue timing. A case
that breaks four others is worth less than none -- #1391's lesson. The
verification above is by hand on Win64 instead.
Smoke: 314 dispatched, 1471 succeeded, 17 failed -- the known
build-configuration failures on this box (exp3, UNIX_DIGEST,
REALITY_LVLS), unchanged from master.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the sweep the issue called for. mux_atol returns long, which
is 32-bit on LLP64, so every caller silently truncated on Windows. Two
of those were real defects (the truthiness family and cf_size, fixed in
the preceding commits); the rest were latent, waiting for a value large
enough to matter.
Rather than audit 290 sites for whether each can reach 2^31 today, use
the 64-bit parser everywhere and remove the class. A dbref cannot
overflow now, but nothing stops a later caller passing that same site a
timestamp or a byte count.
Pure 1:1 substitution: 285 lines changed, and every removed line
contained mux_atol while every added line contains mux_atoi64. No
control flow, no types, no behaviour beyond the wider parse.
This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so
the generated code there is unchanged. It only widens the parse on
Windows. Narrowing destinations are unaffected either way: `int x =
mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both
models.
Left alone: mux_atol itself in mathutil, its declaration, and three
comments that name it. Callers that genuinely want 32-bit semantics can
still ask for them; none appear to.
Verified on Windows: full solution builds clean with no new warnings,
smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched --
identical to before the sweep, with the same 16 build-configuration
failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST).
Spot checks after the change: the boolean family returns 1 for multiples
of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited,
and arithmetic, string and list functions are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Memory-safety pass over previously unsurveyed engine subsystems. Six
confirmed, adversarially-verified bugs, all reachable from untrusted
player input:
- scramble()/shuffle(): Fisher-Yates index arrays were sized LBUF_SIZE/2
but the cluster/word count can reach LBUF_SIZE, so a ~32 KB argument
overflowed the stack array. Both are CA_PUBLIC. (#845)
- view_atr()/flags(): decode_attr_flags() can emit NUM_ATTRIBUTE_CODES+1
bytes but two callers gave it an 11-byte buffer. (#846)
- do_reference()/absolute_named_reference(): used snprintf's return value
(intended length) directly as a copy/compare length, reading past the
buffer on truncation; @reference could leak the adjacent heap bytes back
via @reference/list. Clamped to min(n, LBUF_SIZE-1). (#847)
- @cron value/step parsing accumulated digits into a signed int without an
overflow guard (UB). (#848)
- index(): trailing-space trim decremented before testing, reading the byte
before the lbuf when an item begins at the delimiter. (#849)
Adds regression tests TC012-TC014 to overflow_inject_fn.mux. Build clean,
all 1258 smoke tests pass. Coverage recorded in
docs/survey-memsafety-pass-2026-06.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use the new LBuf_Adopt() macro to take RAII ownership of caller-owned
pool buffers returned by atr_get, atr_pget, and atr_get_LEN. This
eliminates ~82 explicit free_lbuf calls and automatically covers
early-return paths that previously required careful manual placement
of the free.
Heaviest conversions: player.cpp (13), comsys.cpp (14), command.cpp (9).
Complex interleaved patterns (did_it charge/runout swaps, PureName
reassignment, process_cmdent loops) left manual for now.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Second wave: convert manual alloc/free pairs in cque (1), db_rw (4),
conf (5), db (2), engine_com (6), help (7), levels (2), predicates (5),
player (7), funmath (8). Eliminates ~80 explicit free_lbuf calls
including multi-exit error paths in getboolexp1 (5 frees → 0),
get_list, AnnounceConnect/Disconnect, and the eight NOEVAL function
variants (cand/cor/firstof/allof and bool variants).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminate the ISOUTOFMEMORY macro that unconditionally aborted on allocation
failure. Each of the 23 call sites now handles OOM appropriately:
- Fatal sites (buffer pools, db array, anum table): mux_assert or OutOfMemory
- Recoverable sites (queue, mail, commands, guests, vattrs, config, restart,
forward lists): log the failure and return gracefully
Also fix g_dump_child_pid portability: volatile pid_t -> volatile sig_atomic_t
with explicit casts in ganl_adapter.cpp.
Close integer overflow issue as false alarm (getstring_noalloc uses a bounded
static buffer, so nBuffer+1 cannot wrap).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LogStatBuf() was defined but never called. GAME_DOOFERMUX was never
defined, making all four #ifdef blocks dead code (A_REGINFO attribute,
RegInfo table entry, registration stamp on player create, and alternate
execl argv[0]). Update smoke test expectations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LBuf class to alloc.h: an RAII wrapper around alloc_lbuf/free_lbuf
that moves LBUF_SIZE buffers from the stack to the heap pool. Convert
all 108 non-static UTF8 xxx[LBUF_SIZE] stack arrays across 25 source
files. Static BSS buffers (24) are unchanged.
This eliminates LBUF_SIZE from recursive stack frames, making it safe
to increase LBUF_SIZE without risking stack overflow in the evaluation
pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stubslave: The child side of the socketpair was blocking, so read()
would hang when poll() woke for POLLOUT without POLLIN. Set fd 0 to
O_NONBLOCK in main() so read() returns EAGAIN in that case.
@switch: The iter context propagation now checks mudconf.safer_iter
before capturing ##, matching @dolist's existing guard. Games with
safer_iter enabled will not leak ## into queued @switch bodies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@switch (without /inline) passed nullptr for the iter_token when
queuing its body via wait_que, discarding the ## context from any
enclosing @dolist. This broke Myrddin's BBoard +bbread and any
other softcode using ## inside @switch inside @dolist.
Now @switch preserves mudstate.itext[in_loop-1] into the queued
command, matching @dolist's own behavior. Both the match case and
default case paths are fixed.
Add dolist_cmd.mux smoke tests (2 cases) verifying ## survives
into queued @switch bodies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite notify_check() from mux_string to raw PUA-encoded UTF-8 lbufs.
The engine now passes PUA strings through; the network layer converts
PUA to ANSI/XTERM/HTML per client. html_escape() replaces encode_Html(),
co_strip_color() replaces export_TextPlain() for @listen matching.
Remove mux_string overloads: raw_notify, raw_notify_html,
send_text_to_player (session.cpp, conn_bridge.cpp, net.cpp).
Convert handle_ears, look_for_exits (predicates.cpp), notify_comsys
(comsys.cpp), and small uses in mail.cpp/match.cpp.
Add extern "C" guards to color_ops.h for C++ inclusion.
593/593 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>