Commit graph

1959 commits

Author SHA1 Message Date
Yucong Sun
dfac3b2380 Revert "fix: cap optimize()/i_generate_node() recursion depth"
This reverts commit 117cbc1875.
2026-07-14 17:09:42 -07:00
Claude
5c5cdac623 fix: guard pcre_replace_* regression tests with __PACKAGE_PCRE__
PACKAGE_PCRE is off in the WASM build (libpcre isn't cross-built
there -- see src/wasm/README.md's package matrix), same as every
other pcre_*.lpc test file already guards for. These two new tests
called pcre_replace()/pcre_replace_callback() unconditionally, so
they failed to compile under WASM CI.
2026-07-14 13:37:57 -05:00
Claude
1d147bcca4 fix: clear the shared compile log before checking it in inherit_pct_filename_warn.lpc
LOG_DIR + "/compile" is appended to by every file the suite compiles
and is never truncated; read_file() only returns the first
2 * __MAX_READ_FILE_SIZE__ bytes of a file. In a full 596-file suite
run, enough prior warnings can accumulate that this test's freshly
appended entry falls outside that window, intermittently failing the
tag-presence check regardless of test-file naming. Clear the log
immediately before triggering the warning so the check only ever needs
to find the tag in a small, fresh file.
2026-07-14 13:37:57 -05:00
Claude
a3a009d9d8 fix: avoid a shared function name in inherit_pct_filename_warn.lpc
The generated parent/child sources used the extremely common function
name "foo", which dozens of unrelated test files in the same suite run
also declare. The compiler's identifier table is shared process-wide
across every file compiled in one run; use a name unique to this test
to remove any risk of cross-file interference with the previous-
prototype lookup this test depends on for its return-type-mismatch
warning to fire.
2026-07-14 13:37:57 -05:00
Claude
251dfad9a2 fix: guard dwlib_replace_objects.lpc test with __PACKAGE_DWLIB__
PACKAGE_DWLIB defaults OFF and only two of the CI matrix's twelve
native jobs turn it on (-DPACKAGE_DWLIB=ON, the Ubuntu GCC jobs).
The regression test added for replace_objects() called the efun
unconditionally, so on every other job the test file failed to
compile (undefined function), matching the existing convention (see
domain_stats.lpc) of guarding optional-package tests with
#ifdef __PACKAGE_*__.
2026-07-14 13:37:57 -05:00
Claude
d23d24172f change: mask shift counts mod 64 instead of erroring
'<<'/'>>' with a shift count that's negative or >= 64 is UB for LPC's
64-bit int. Previously fixed by rejecting the count with a clean
error(); switched instead to masking the count to its low 6 bits
(`& 63`) before shifting, matching Java's `long` shift semantics (JLS
15.19). This keeps every LPC int a legal shift count with a
deterministic result (e.g. `1 << 100` == `1 << 36`) rather than making
an otherwise-ordinary script error out for shifting by a large or
negative amount.

Applied consistently across all three parallel sites so a folded and
unfolded shift by the same count always agree: the runtime efuns
(f_lsh/f_rsh/f_lsh_eq/f_rsh_eq, ops.cc), the compile-time constant
folder (trees.cc's binary_int_op()), and the #if preprocessor
evaluator (lexer_rules_pp.cc's ifexpr_binop()).

Updated shift_overflow.lpc to assert the masked values instead of a
caught error, and AGENTS.md's audit-checklist entry to describe masking
rather than guard-and-error for shifts specifically (the INT_MIN/-1
divide/modulo guards are unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
fd7ae2704d docs: capture round-2 review discoveries in AGENTS.md/README
- error() is a genuine C++ throw (simulate.cc's error_handler()/
  restore_context() all end in throw(...), confirmed by CMake's own
  WASM-target comment about relying on C++ exceptions), not a raw
  longjmp -- so RAII destructors DO fire reliably across an error()
  unwind. Corrected the §4 framing, which previously called this
  "longjmp" while also telling readers to rely on RAII (self-
  contradictory, since longjmp doesn't run destructors).
- The VM's generic efun-dispatch (F_EFUN1/2/3/V) already validates any
  argument whose spec type is a scalar/reference type or union thereof
  before the efun body runs -- documented in §2 so a "missing type
  check" finding gets verified empirically before being treated as
  real (it can only miss per-element/varargs-position checks).
- -fwrapv's actual scope: it defines +/-/*/negate overflow (confirmed
  consistent across interpret.cc's runtime opcodes and the #if
  evaluator; trees.cc's constant folder doesn't fold these three at
  all), but does NOT cover shift-count or INT_MIN/-1 UB, and "defined"
  overflow of a size/offset/count is still not memory-safe -- it still
  needs an explicit bound check before an allocation/memcpy/index.
- Three new recurring bug-class entries: sentinel-value collisions
  (math.cc), subtype/side-channel state left stale after a compound
  assign (ops.cc), and unmetered backtracking search invisible to
  max_eval_cost (parser.cc) -- plus the "abort via plain return, not
  error(), when a deep C-style call chain has per-frame cleanup"
  pattern used for the parser fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
3085be0089 fix: MySQL_connect() leaks internal buffers on failed connect
MySQL_connect() (src/packages/db/db.cc) called mysql_init() into a
DMALLOC'd wrapper struct, then on a mysql_real_connect() failure just
FREE'd that wrapper -- skipping mysql_close(), which is what releases
the internal buffers mysql_init() allocated. The sibling failure
branch (mysql_select_db() failing) already calls mysql_close() first;
this mirrors that.

This is the same MySQL leak that's been showing up as an intermittent
"unrelated" ASan LeakSanitizer failure throughout this review whenever
test ordering ran single/tests/efuns/db_connect.lpc's unreachable-host
connect attempt -- confirmed by reproducing it in isolation (152 bytes
in 2 allocations from mysql_init() at db.cc:774) and confirming it's
gone with this fix, across several full-suite reruns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
7a47120207 fix: ops.cc compound-assign efuns don't clear the lvalue's subtype
"undefined" (the value read back from a missing mapping key) is
represented as {T_NUMBER, subtype=T_UNDEFINED, 0}. The T_NUMBER cases
of &= /= %= *= |= ^= <<= >>= -= (src/packages/ops/ops.cc) modify the
lvalue's u.number in place via a compound C operator but never reset
its subtype, so a variable that started out undefined kept reporting
undefinedp() == 1 after a real arithmetic result had been computed
into it. f_xor_eq() didn't even clear the pushed result's subtype
either. The VM's own F_ADD_EQ (+=) already got this right for
comparison.

Also cleared f_or()'s result subtype to match its sibling f_and() --
same root cause (a plain `|` on an undefined left operand reused that
operand's stack slot, carrying its subtype into the real result).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
cdb1e9e923 fix: f_ffi_struct_layout() leaks offs array on unknown type code
f_ffi_struct_layout() (src/packages/ffi/ffi.cc) manually freed its
partially-built `offs` array before erroring on a non-int field type,
but type_size() (via code_to_type()) itself error()s on an
unrecognized FFI type code -- a path that skipped that cleanup
entirely, leaking `offs`.

Switched to a unique_ptr with free_array as the deleter, the same
pattern already used in f_ffi_callback() in this file, so every
error() exit (bad field type or bad type code alike) is covered
uniformly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
04fd64b466 fix: socket_write() leaks uninitialized heap memory for T_ARRAY sends
f_socket_write()'s T_ARRAY branch (src/packages/sockets/socket_efuns.cc)
allocates its output buffer with DMALLOC (uninitialized) and only fills
each 4-byte slot when the corresponding array element is T_NUMBER or
T_REAL; any other element type left its slot untouched. That buffer is
then sent to the remote peer as-is, disclosing whatever uninitialized
heap memory happened to be in each skipped slot.

Non-numeric elements now zero-fill their slot instead, preserving the
existing "silently skip" behavior for well-defined values sent on the
wire.

Verification note: the driver's own socket tests deliberately avoid a
live self-connection (pending events can wedge some event loops -- see
socket_connect.lpc), and this branch is only reachable through an
actual connected STREAM socket, so no automated regression test was
added. Verified instead with a minimal standalone reproduction of the
exact pattern (DMALLOC + selective memcpy + write()) under valgrind:
the unfixed pattern reports "Syscall param write(buf) points to
uninitialised byte(s)"; the fixed pattern (memset-ing skipped slots)
reports zero errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
9260e704e5 fix: mudlib_stats.cc save_stat_list() unbounded sprintf
save_stat_list() built its output path with sprintf() into a fixed
char fname_buf[MAXPATHLEN], while its sibling restore_stat_list()
already builds the same kind of path with snprintf(). Its only
current callers pass fixed literal filenames ("domain_stats",
"author_stats"), so this isn't reachable with attacker-controlled
data today, but the pattern is a real hazard for any future caller and
inconsistent with the sibling function -- switched to snprintf() to
match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
06ff4ed073 fix: math.cc sentinel-value collision and printf format/arg mismatch
norm()/vector_op() (src/packages/math/math.cc) signaled errors (bad
element type, mismatched vector sizes) by returning special sentinel
LPC_FLOAT values (-INT_MAX, -INT_MAX+1, -INT_MAX+2) instead of the
real result. A legitimate dotprod() of large-magnitude vectors (or an
angle() built on one) can land exactly on one of those values -- e.g.
dotprod(({-2147483646, 0}), ({1, 0})) == -2147483646 == -INT_MAX+1 --
and got misreported as "invalid arg" instead of returning the correct
result.

Also, the error() calls formatted that same LPC_FLOAT sentinel with
"%d" (dotprod/distance/angle all did this) -- a printf format/arg
type mismatch (UB) present regardless of the collision bug, since
error() has no format-checking attribute to catch it at compile time.

Replaced the sentinel-encoded errors with explicit out-parameters
(bool* for norm(), an int status code for vector_op()/dotprod()) so a
real computed value is always returned on success, and error messages
format a real int status code with "%d".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
02a1424a92 fix: parser.cc unbounded recursion, error-path leak, and backtracking DoS
Three issues in src/packages/parser/parser.cc:

- add_objects_from_array()/get_objects_from_array() recursed into every
  nested T_ARRAY element of the caller-supplied `environment` argument
  to parse_sentence()/parse_init() (parse_env) with no depth cap, so an
  arbitrarily deep-nested array overflows the C stack. Capped at
  MAX_PARSE_ARRAY_DEPTH (100).

- do_the_call() calls error("...no do_* function found...") when none
  of the 4 candidate "do_" applies exist on the matched object. That
  error() longjmps out while best_result still held a ref on the
  handler object and its unfreed res[].func name strings -- leaked
  until some later, unrelated parse_sentence()/parse_my_rules() call
  happened to free the stale global. Now frees best_result (capturing
  the object's name first, since dropping the ref can free it) before
  erroring.

- parse_rule()'s backtracking search retries every word-split for each
  non-terminal STR/OBJ/OBS/LIV/LVS token in a rule, recursing into the
  rest of the rule at each split; make_rule()'s "two object tokens per
  rule" cap doesn't apply to STR_TOKEN, so a rule chaining many STR
  tokens against a long sentence makes the search combinatorial in the
  word count -- and none of it is charged against the normal LPC
  eval-cost limiter (plain C recursion, not bytecode). Added a step
  counter that, once tripped, unwinds every pending parse_rule() call
  via a plain return (not error(), which would leak parse_recurse()'s
  pending per-frame allocations the same way as the do_the_call() bug
  above) -- indistinguishable from an exhausted, no-match search to the
  caller.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
cce39ab88f fix: jsbridge.cc mark_callback_refs() corrupts non-shared strings
mark_callback_refs() (src/packages/jsbridge/jsbridge.cc, WASM-only
package, DEBUGMALLOC_EXTENSIONS debug-checker mark phase) unconditionally
treated any T_STRING call_back as a STRING_SHARED block -- bumping
".extra_ref" through EXTRA_REF(BLOCK(...)), which assumes a block_t
header immediately precedes the string's char data.

call_back is populated via assign_svalue_no_free() (js_call()/js_export()
copying the caller's callback argument), which only produces a ref-counted
copy for STRING_MALLOC/STRING_SHARED -- a literal callback name argument
(e.g. js_call(..., "my_callback")) is copied as STRING_CONSTANT, pointing
directly at read-only literal data with no block_t header at all. Bumping
a fabricated header field there is a stray write into whatever precedes
the literal in memory.

Mirrors checkmemory.cc's mark_svalue(): dispatch on subtype, using
MSTR_EXTRA_REF for STRING_MALLOC and EXTRA_REF(BLOCK(...)) for
STRING_SHARED, and touching nothing for STRING_CONSTANT.

Verification note: this package only builds under Emscripten
(src/packages/jsbridge/CMakeLists.txt gates it on
`EMSCRIPTEN AND PACKAGE_JSBRIDGE`), and this environment has no emsdk
installed, so this fix could not be exercised through an actual
build+testsuite cycle. Verified by code review against the identical,
already-proven dispatch in checkmemory.cc's mark_svalue() (used
correctly by every other T_STRING mark site in the codebase) and by
confirming MSTR_EXTRA_REF/EXTRA_REF/BLOCK are available the same way in
other package files (e.g. packages/parser/parser.cc) via just
base/package_api.h, same as jsbridge.cc already relies on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
0172da2a48 fix: move_object() missing destruct recheck after lazy try_reset()
move_object() (src/vm/internal/simulate.cc) calls try_reset(dest) to
lazily run dest's reset() before linking item into dest's inventory.
reset() is arbitrary LPC and can destruct dest as an entirely ordinary
side effect (not an error(), so safe_apply() inside try_reset() doesn't
catch it) -- move_object() never rechecked O_DESTRUCTED afterward, so
it went on to link item into (and set item->super to) an object that
was no longer live, corrupting the object graph. Mirrors the existing
post-call_create() recheck in clone_object().

Added a gtest (DriverTest.TestMoveObjectDestructDuringReset) since
reproducing this requires direct control over next_reset/the game
tick and the "lazy resets" config flag, which isn't practical to
exercise from the LPC testsuite's fixed boot config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
88e1fe510b fix: terminal_colour() stack exhaustion from per-segment alloca()
f_terminal_colour() (src/packages/contrib/contrib.cc) called alloca()
once per %^-delimited segment inside its main processing loop to hold
the terminal_colour_replace() apply's return value. alloca'd memory is
not reclaimed until the whole function returns, and parts[i] can alias
that memory well past the iteration that created it (it's read again in
the composition pass at the end of the function), so it can't simply be
freed per-iteration either. With many segments and/or a replace apply
that returns long strings, the accumulated alloca'd stack usage grows
for the entire call and can exceed the thread stack.

Replaced with heap allocations tracked in a new rep_allocs[] array
(parallel to parts[]/lens[]) and freed together at the point parts[]
already gets freed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
8b7488d668 fix: compress.cc stack overflow and unbounded decompression growth
f_compress_file()/f_uncompress_file() strcpy()'d the check_valid_path()
result (no length bound) into a fixed 1024-byte outname[] stack buffer;
an arbitrarily long path overflowed it before any filesystem check ran.
Also fixed f_compress_file()'s !real_output_file branch, which called
FREE_MSTR(output_file) unconditionally even when output_file was the
2-argument form's stack-owned string, not one we allocated.

f_uncompress() accumulated decompressed size into a plain `int len`
across the whole inflate() loop with no bound, only rejecting an
oversized result via allocate_buffer()'s cap *after* fully expanding it
-- letting a small, highly compressible input drive unbounded
intermediate allocation/copy work (and, past ~2GB output, overflow
`len` itself). Now checks `len` against __MAX_BUFFER_SIZE__ every
iteration and bails out immediately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTPLudxra3u1andNyWJEQG
2026-07-14 13:37:57 -05:00
Claude
fa54f063af fix: guard <</>> shift operators against out-of-range shift counts
LPC ints are 64-bit (int64_t); a shift count that is negative or >= 64
is undefined behavior in C++ for both '<<' and '>>'. Three unguarded
sites, mirroring the INT_MIN/-1 divide/modulo pattern fixed previously:

- src/packages/ops/ops.cc: the runtime shift efuns f_lsh/f_rsh/
  f_lsh_eq/f_rsh_eq, reachable with a fully attacker-controlled shift
  count from ordinary LPC (`x << n`, `x <<= n`).
- src/compiler/internal/trees.cc: the compile-time constant folder
  (binary_int_op), reachable via a literal shift expression like
  `1 << 100` in mudlib source.
- src/compiler/internal/lexer_rules_pp.cc: the #if preprocessor
  expression evaluator's 's'/'S' (<</>>) cases -- already guarded for
  '/' and '%' a few lines below, but not shifts.

Each now rejects an out-of-range shift count with a clean, catchable
error (efuns) or compile error (constant folder, #if evaluator)
instead of trapping. Regression test covers all three sites plus the
ordinary in-range/constant-fold cases to confirm unchanged behavior;
confirmed UBSan traps at ops.cc's f_lsh on the unfixed binary.
2026-07-14 13:37:57 -05:00
Claude
04820a5c28 fix: bound PORT_TYPE_ASCII reads by remaining room in ip->text, not buf
get_user_data()'s "how much can we read right now" switch explicitly
computes text_space from the actual remaining room in ip->text for
PORT_TYPE_TELNET (via comm_reserve_input_space()) and PORT_TYPE_MUD,
but PORT_TYPE_ASCII had no case and fell through to `default:`, which
just uses the local scratch buffer's full size (sizeof(buf), 1MB).
PORT_TYPE_ASCII accumulates each read into ip->text at offset
ip->text_end (looking for a newline before dispatching a command), so
a client that withholds a newline across repeated reads can grow
ip->text_end close to MAX_TEXT and then have the next read's memcpy
overflow ip->text -- a network-triggerable, pre-authentication buffer
overflow.

Give PORT_TYPE_ASCII the same comm_reserve_input_space() call already
used for PORT_TYPE_TELNET, so the read is always bounded by actual
remaining space (which also handles compaction/eviction the same way).
No dedicated live-network regression test: the existing
socket_connect.lpc test explicitly avoids a real self-connect in this
harness because it can wedge the event loop, so this is verified via
code review (the fix exactly mirrors the adjacent, already-correct
PORT_TYPE_TELNET case) plus the full suite, which already exercises
real telnet/websocket connections end to end.
2026-07-14 13:37:57 -05:00
Claude
cc6b97cbec fix: compute pcre_match()'s is_string from the actual subject slot
f_pcre_match() read (sp - 1)->type to decide whether the subject is a
string or an array, before any optional trailing arguments (flag,
pcre_flags) were popped off the stack. That only located the subject
for the 2-argument call form; with a 3rd argument present, sp - 1 is
actually the pattern (always a string), so ANY 3-argument call --
including the documented array-mode form pcre_match(array, pattern,
flag) -- was misidentified as string-mode. This misrouted the call
into pcre_match_single() with the array's svalue reinterpreted as a
string (type confusion) and left the array's reference uncounted
(freed via free_string_svalue(), never free_array()), returning a
wrong-shaped result.

Compute is_string from sp - st_num_arg + 1 (the subject's fixed
position regardless of trailing-argument count) instead. Add
regression tests for the 3- and 4-argument array-mode forms; confirmed
the unfixed binary drops the array's reference (debug ref-checker:
"Bad ref count for array, is 1 - should be 0") on the 3-arg case.
2026-07-14 13:37:57 -05:00
Claude
0115186f9d fix: bound the C-stack cost of freeing deeply nested arrays/mappings/classes
int_free_svalue()'s dealloc_array()/dealloc_mapping()/dealloc_class()
calls loop over their children calling free_svalue(), which recurses
straight back into int_free_svalue() whenever a child is itself a
compound value hitting ref 0 -- unboundedly, for structures like a
singly-nested array chain built by `mixed a = 0; for (...) a = ({a});`.
That's cheap, unprivileged LPC to construct (O(N) small headers) and
crashes the driver (stack overflow, uncatchable) the moment the last
reference drops and the whole chain unwinds recursively.

Rather than a depth-limited reject (this path is unwinding memory that
must still be freed either way -- refusing would leak, not just reject
a request), give int_free_svalue() an explicit iterative worklist: only
the outermost T_ARRAY/T_CLASS/T_MAPPING deallocation actually calls
dealloc_*() synchronously; any compound child discovered while one is
already running is queued instead of recursed into, and drained in a
loop after the outer call returns. This keeps C-stack usage O(1)
regardless of nesting depth or shape (chains, wide trees, or both), for
all three container types, with no change to array.cc/mapping.cc/
class.cc or to the end state of what gets freed.

Add a regression test (200000-deep array chain, 50000-deep mapping
chain) that reproduces the crash on the unfixed binary. The mapping
depth is capped lower than the array one: each nested mapping here
shares one literal key, and a shared string's ref count is a 16-bit
field (block_t::refs) -- a separate, pre-existing ceiling that a much
higher simultaneous-reference count would spuriously trip, unrelated
to this fix.
2026-07-14 13:37:57 -05:00
Claude
117cbc1875 fix: cap optimize()/i_generate_node() recursion depth
A left-nested chain of binary/unary/ternary operators (e.g. tens of
thousands of '+' terms seeded by a non-constant operand, so constant
folding can't collapse it) builds a parse tree as deep as the input --
LALR parsing itself stays shallow since each operator reduces
immediately, but the post-parse optimizer (optimize(), generate.cc)
and code generator (i_generate_node(), icode.cc) then walk that tree
with plain recursion and no depth cap, overflowing the C stack at
compile time from ordinary mudlib source (e.g. via write_file() +
load_object()).

Add a depth counter to each, mirroring MAX_INCLUDE_DEPTH's role for
#include nesting: i_generate_node() reports a clean compile error and
stops recursing past the cap (letting num_parse_error abort the
compile normally); optimize() just stops optimizing the excess depth
and returns the subtree as-is, which is behavior-preserving since
skipping that optimization doesn't change program semantics.

The depth threshold needed to be well under a "plausible" value: under
this build's ASan instrumentation each stack frame is large enough
that a depth cap of 4000 still overflowed the stack before being
reached, confirmed by testing (500 is comfortably safe here; verified
with a 3x full-suite re-run for regressions in legitimately deep
existing code).
2026-07-14 13:37:57 -05:00
Claude
c4dbb17c96 fix: reject string callbacks and heap-copy bound args in async efuns
async_db_exec()'s spec allows `string | function` for its callback and
a trailing `...` for bound args, but f_async_db_exec() (and, for the
callback-type issue, f_async_read/f_async_write/f_async_getdir) never
checked which form process_efun_callback() actually populated:

- A string-named callback sets function_to_call_t::ob (non-null) and
  ->f.str (a char*), not ->f.fp; the unconditional `cb->f.fp->hdr.ref++`
  reinterpreted that char* as a funptr_t*, corrupting memory. These
  efuns' callbacks fire asynchronously (worker thread + later game
  tick), unlike the synchronous consumers of this same struct (pcre,
  array map/filter/sort) that make the string form safe -- supporting
  it properly would need its own reference-counted object/string
  lifetime. Reject it with a clean error() instead.

- Trailing bound args are captured by process_efun_callback() as a raw
  pointer into the live VM stack (safe only for synchronous consumers).
  async_db_exec's callback runs long after that stack frame is gone, so
  the pointer was stale by the time it fired; the stack cleanup also
  only popped one slot regardless of how many bound args were passed.
  Heap-copy them into a new array (mirroring call_out()'s cop->vs) with
  matching cleanup in check_reqs() and marking in async_mark_request().

Add a regression test covering both: the string-callback form errors
cleanly, and a bound arg survives correctly to the callback. Confirmed
against the unfixed binary (string form silently proceeds and corrupts
subsequent driver state instead of erroring). Full suite re-run 3x
clean given the threading-sensitive nature of this file.
2026-07-14 13:37:57 -05:00
Claude
beb68b1142 fix: string-char lvalue reentrancy and missing/broken inc-dec write-back
Two related bugs in the shared string-char (T_LVALUE_CODEPOINT) lvalue
machinery (src/vm/internal/base/interpret.cc):

- A foreach(... ref c in a_string) loop read the ref's value straight
  off the single shared VM scratch slot (global_lvalue_codepoint)
  instead of state private to the ref. Any other string-char lvalue
  operation in the loop body -- even on an unrelated string -- retargets
  the shared slot and silently corrupts what the ref reads next. Fixed
  by giving each ref_t its own codepoint_owner/codepoint_index, set at
  every site that arms a ref onto the shared sentinel (foreach and the
  `f(ref s[i])` call-site form), with F_REF reading from the ref's own
  state and F_REF_LVALUE re-arming the shared scratch right before a
  write consumes it.

- F_PRE_INC, F_POST_INC and F_POST_DEC never wrote their result back to
  the stack for a string-char lvalue, leaving a raw internal T_LVALUE
  svalue as the expression's value (e.g. `int x = s[0]++;` corrupted
  `x`'s type). F_POST_INC's lambda also used postfix `c++`, which
  returns the OLD codepoint as the "new" one -- the character was never
  actually incremented. F_PRE_DEC had no T_LVALUE_CODEPOINT case at
  all, throwing "-- of non-numeric argument" for `--s[i]`. All four now
  match the sibling T_LVALUE_BYTE arms: capture the correct pre/post
  value and write it back as T_NUMBER.

Add regression tests reproducing both the corruption/wrong value and
the crash on the unfixed binary. Also required a fix to F_MAKE_REF
(the `f(ref s[i])` call-argument path), which copies whatever lvalue is
already armed on the stack without knowing about the new per-ref
codepoint fields; verified against a regression in testsuite's
existing single/tests/operators/ref.lpc during a 3x full-suite
re-run, confirmed fixed by capturing owner/index there too.
2026-07-14 13:37:57 -05:00
Claude
a82bdd3020 fix: handle self-destruct during reload_object(), guard free_object() null
f_reload_object() called reload_object() (which runs the target's
create() -- arbitrary LPC that can destruct(this_object())) and then
unconditionally free_object()'d its own VM-stack argument slot.
destruct_object() sweeps the VM stack, so that slot may already hold a
plain 0 by the time control returns. Switch to free_svalue(), mirroring
the identical, already-fixed hazard in the adjacent f_recompile_object().

Independently, free_object() itself had an inconsistent null guard: it
skipped the ref-- for a null *ob but then unconditionally dereferenced
(*ob)->ref on the next line, turning what should be a safe no-op into a
guaranteed crash for any current or future caller that passes an
already-cleared slot. Fix the guard to actually cover both statements.

Add a regression test (an object whose create() self-destructs on its
second invocation, reached via reload_object()) that reproduces the
crash on the unfixed binary.
2026-07-14 13:37:57 -05:00
Claude
2bdfd93838 fix: validate matrix transform array elements are T_REAL
translate/scale/rotate_x/rotate_y/rotate_z/lookat_rotate/lookat_rotate2
(src/packages/matrix/matrix.cc) only checked the input array's size
(>= 16), never that each element was actually T_REAL, before reading
AND overwriting matrix->item[i].u.real in place. A non-float element
(e.g. a string) has its type/pointer union corrupted in the caller's
own array -- a type-confusion primitive reachable from ordinary LPC.
Add a per-element T_REAL check to all seven functions and a regression
test that reproduces the corruption (and resulting crash) on the
unfixed binary.
2026-07-14 13:37:57 -05:00
Claude
1994acca19 fix: validate pcre_replace() argument/element types, fix leaks on error paths
Five related pcre.cc defects fixed together:

- f_pcre_replace() never checked its 3rd argument was actually T_ARRAY
  (the .spec type is compile-time-only and a `mixed` value bypasses
  it), reinterpreting a non-array svalue's union bits as an array_t*
  and crashing on the first dereference.
- Each replacement array element was likewise never checked for
  T_STRING before pcre_get_replace() read its .u.string, causing the
  same kind of type-confusion crash for e.g. an int element.
- f_pcre_replace_callback() allocated its substrings array before
  validating the (optional-callback) 3rd argument, leaking it on the
  "Illegal third argument (0)" error path.
- pcre_match()'s raw `res` scratch buffer had no RAII guard, leaking it
  whenever the result array size exceeded __MAX_ARRAY_SIZE__ and
  allocate_empty_array() error()'d.
- pcre_assoc() built its per-pattern pcre_t array and RegMatch chain
  with no RAII guard either; the same array-size error path leaked
  both. Fixed by validating the size before allocating the result
  arrays (reusing the same manual-cleanup shape already used by this
  function's compile-failure path) instead of letting the error fire
  mid-construction.

Added regression tests for the two type-confusion crashes (reproduced
against the unfixed binary). The two ancillary leak fixes (pcre_match/
pcre_assoc) are small, mechanical, and mirror an already-established
pattern in this same file; they aren't independently regression-tested
here because reproducing them requires arrays large enough to also
trip an unrelated, pre-existing debug-ref-checker gap in this driver's
handling of large array arguments across nested efun calls (separate
from anything touched here -- worth its own follow-up).
2026-07-14 13:37:57 -05:00
Claude
6e359e590d fix: pass return-type-mismatch warning as a yywarn() argument, not format
define_new_function() built a warning embedding the inherited-from
program's filename and passed the resulting buffer straight to
yywarn(buff), which is printf-style -- a '%' in the filename (an
ordinary, mudlib-choosable path) is interpreted as a conversion
specifier instead of literal text, reading unrelated varargs and
potentially crashing. Pass it as yywarn("%s", buff), mirroring the
identical fix already applied at the sibling multiple-inheritance
warning site. Add a regression test that reproduces the crash on the
unfixed binary and confirms the filename now appears literally in the
logged diagnostic.
2026-07-14 13:37:57 -05:00
Claude
86592aa76b fix: guard #if expression evaluator against INT_MIN / -1 SIGFPE
ifexpr_binop()'s '/' and '%' operators (src/compiler/internal/lexer_rules_pp.cc)
only guarded against a zero divisor. Dividing/modulo-ing LONG_MIN by -1
is undefined behavior that traps (SIGFPE) on x86, crashing the whole
driver at compile time from an ordinary #if expression in mudlib
source. Special-case divisor == -1 the same way the interpreter's
F_DIVIDE/F_MOD and the compiler's constant folders already do, and add
a regression test that reproduces the trap on the unfixed binary.
2026-07-14 13:37:57 -05:00
Claude
28b6e2920f fix: check set_bit()'s bit index for sign before 32-bit truncation
f_set_bit() checked sp->u.number > max_bitfield_bits on the untruncated
64-bit value but only checked bit < 0 after narrowing into a 32-bit
int. A large-magnitude negative index (negative, so it passed the
upper-bound check) could truncate into a positive bit index exceeding
__MAX_BITFIELD_BITS__, bypassing the cap entirely and driving an
unbounded new_string() allocation. Move the sign check to the
untruncated value, and add a regression test pinning that such a value
is rejected.
2026-07-14 13:37:57 -05:00
Claude
ce84c0b7b6 fix: bound replace_objects() buffer copies against master-apply strings
replace_objects() (src/packages/dwlib/dwlib.cc) formatted an object's
name plus the mudlib master's object_name() apply result into a fixed
2000-byte stack buffer via strcpy/strcat with no length check. Since
object_name() returns arbitrary mudlib-authored string data, a long
enough result overflows the buffer. Replace the raw copies with a small
bounded-append helper (mirroring the existing pattern in
mudlib_stats.cc) and add a regression test that reproduces the ASan
stack-buffer-overflow on the unfixed binary.
2026-07-14 13:37:57 -05:00
Claude
d13d03a01e fix: widen load_object() real_name/obname buffers to prevent stack overflow
filename_to_obname() can fill name[400]/actualname[400] with up to 399
bytes of content; real_name/obname were only sized name+2 (402 bytes),
2 bytes short of the 404 needed for a 399-byte name plus the 4-byte
".lpc"/".c" extension and NUL. An ordinary, unprivileged load_object()
call with a long extension-less path overflowed the stack buffer via
strcat(). Widen the buffers to name+5 and add a regression test that
reproduces the ASan stack-buffer-overflow on the unfixed binary.
2026-07-14 13:37:57 -05:00
Claude
3bdbe9676e www: real ws backpressure fix, teardown leak fix, src/www docs, websocket smoke test in CI
The lws output wedge addressed in the previous commit was incompletely
understood: under genuine backpressure (peer slow or paused, kernel
send buffer full) a connection still froze permanently. Root cause,
established by tracing the writeable-request plumbing end to end:
lws_send_pipe_choked() is true not only when lws holds a truncated
send (lws re-arms the writeable callback itself then) but also when a
zero-timeout poll(POLLOUT) reports the socket simply full -- and in
that case every lws_write() has fully succeeded, lws has nothing
pending, and nobody re-arms anything. Fix, per lws README.coding.md:
whenever the drain loop exits with data still queued in pss->buffer,
request the next writeable callback. Queued data always has a callback
requested, so no exit path can strand output.

Also:
- LWS_CALLBACK_CLOSED frees the session evbuffer unconditionally: on
  driver-initiated closes (e.g. the mudlib destructing the interactive)
  close_user_websocket() nulls pss->user first, and the old early
  return leaked the buffer every time.
- src/www/README.md + src/www/AGENTS.md: architecture doc and agent
  checklist for the web terminal pages (xterm.js/telnet.js layering,
  vendor policy, packaging, testing, the wedge mechanism).
- tools/ws-smoke.js, wired into CI on the Clang Debug matrix entries
  (with and without sanitizers): a dependency-free node websocket
  client that boots the real driver and exercises the http mount,
  telnet + ascii subprotocols through the shared src/www/telnet.js,
  SGA char-mode switching, live TUI streaming, TLS, and -- the actual
  regression gate -- forced-backpressure bursts (paused socket, ~4.8MB)
  on the plain and TLS ports plus a destruct-while-choked teardown
  check. All three backpressure checks fail on the unfixed driver;
  neither GTest nor the LPC suite exercises any websocket client
  traffic.
- Fix stale src/www/wasm/vendor/ path references left from the vendor
  directory move (src/wasm/README.md, docs/build-wasm.md, release.yml).

Validated on the ASan Debug build: forced-backpressure repros recover
the full burst on both subprotocols, destruct-while-choked clean under
ASan, ws-smoke 17/17, GTest 312/312, LPC testsuite clean.
2026-07-14 09:53:26 -05:00
Claude
f45461cb97 web terminals on xterm.js; TUI library fixes; emsdk pin; lws output-wedge fix
One coherent change: make the mudlib TUI library (/std/tui) work in the
browser on BOTH web terminals, harden everything the work surfaced, and
pin the toolchain that broke it.

## Web terminals: xterm.js + a shared telnet client

* Vendor @xterm/xterm 6.0.0 + @xterm/addon-fit 0.11.0 (dist files
  byte-exact from the official npm tarballs, licenses included) under
  src/www/vendor/. xterm.js does the terminal emulation on both pages:
  rendering, SGR 16/256/truecolor, alternate screen, cursor state, wide
  characters, scrollback, mouse reporting, bracketed paste, and keyboard
  encoding (the whole dialect testsuite/std/tui/keys.lpc decodes,
  including C-_ undo).
* src/www/telnet.js -- one telnet option engine shared by both pages
  (transport-agnostic; hooks for page-specific options): ECHO masks the
  password prompt, WILL/WONT SGA -- the driver's char-mode signal
  (set_charmode) -- automatically switches between the line-input bar
  and raw keystroke streaming, NAWS reports real terminal geometry from
  the fit addon and re-reports on resize (driving the window_size
  apply), TTYPE answers xterm-256color.
* src/www/wasm/index.html (the wasm shell): output/input rides xterm.js;
  the page keeps the synchronous-bridge queueing (sends flush outside
  receive() -- the wasm bridge re-enters the parser otherwise), the
  error modal, and the jsbridge handlers. Also guards
  crypto.getRandomValues() against views backed by resizable
  ArrayBuffers (see the emsdk section below).
* src/www/index.html (the websocket client for the native driver):
  rewritten on the same stack, replacing a parseANSI() that stripped all
  cursor sequences (no TUI possible) and a telnet layer whose option
  bytes leaked into the text stream and never answered negotiations.
  Passwords now mask, char mode works, GMCP/MSP kept (dead
  TelnetOverWebSocket/handler classes removed); ws frames arrive as
  arraybuffers (no Blob/FileReader path); UTF-8 and telnet sequences
  survive frame splits (streaming decode + stateful parser).
* tools/wasm/pack-mudlib.sh and the release zip ship vendor/ and
  telnet.js next to index.html in both layouts.

## Driver: websocket output wedged permanently on multi-window bursts

Re-arming the writeable event from inside LWS_CALLBACK_SERVER_WRITEABLE
is lossy with the libevent event lib: after the user callback returns,
lws core clears POLLOUT and its pollfd bookkeeping desyncs from the
evlib watcher -- the request is dropped and every later
lws_callback_on_writable() no-ops, freezing output on that connection
for good. First bites on any burst larger than one 2048-byte window
(e.g. a full-screen TUI frame; no test had ever pushed one through a ws
client). The ws_telnet.cc/ws_ascii.cc handlers now drain the evbuffer in
a loop gated on lws_send_pipe_choked(); a choked write is flushed by
lws's own core-managed POLLOUT path, which fires the callback again.
Found by the browser end-to-end run below; documented in AGENTS.md 14.

## TUI library (/std/tui): review fixes + features

Fixes: wslice() dropped combining marks from every sliced render;
ESC[1;mR (modified F3) misdecoded as a cursor position report; readline
lost the left scroll marker when a line overflowed both viewport edges;
stray mouse events cancelled incremental search; Tab on a unique
already-complete match missed the trailing space; menu lines wider than
the terminal wrapped and desynced the in-place repaint (width now fed by
the glue and re-fed on NAWS resize); the menu overflow indicator only
showed below the window; backward focus cycling from the initial state
skipped the last widget; a terminal-initiated close (disconnect,
tui_destroy) leaked the app clone and its widgets -- teardown now runs
through a reentry-guarded app_quit() in both directions.

Features (from the README's own deferred list): readline C-_ undo
(per-keystroke snapshots); pterm-style type-to-filter in select/
multiselect (results index the original choices); mouse-wheel scrolling
in list/table/tree/log (wheel no longer click-selects); table clicks
honour the header offset; tree Left on a leaf jumps to its parent.

All pinned by testsuite/single/tests/std/tui/fixes.lpc, including the
app teardown cycle via a runtime-written mock terminal.

## Toolchain: pin emsdk, guard random_get()

emsdk 6.0.2 defaulted GROWABLE_ARRAYBUFFERS=1, making every
ALLOW_MEMORY_GROWTH build's heap a resizable ArrayBuffer
(wasmMemory.toResizableBuffer()) in browsers shipping the wasm
rab-integration -- and two emscripten runtime paths pass raw
HEAPU8.subarray() views into Web APIs that reject resizable-backed
views: random_get() -> crypto.getRandomValues() (threw at boot) and
UTF8ToString() -> TextDecoder (broke jsbridge). 6.0.3 reverted the
default AND fixed the string codegen (getUnsharedTextDecoderView ->
getHeapViewOrCopy), but random_get() is still unguarded upstream.
Reproduced and certified against real 6.0.2/6.0.3 toolchains in
Chromium (--js-flags=--experimental-wasm-rab-integration): 6.0.2
unpatched throws the exact boot error, 6.0.2 + the page's
getRandomValues wrapper passes, 6.0.3 passes. CI now installs a pinned
emsdk-ver input (default 6.0.3) instead of "latest".

## Verification

* Native Debug+ASan: GTest 312/312; full LPC suite (574 files, per-file
  ref-count checker) x3 across the work; TUI test dir x3 randomized.
* LPC suite inside the wasm driver under node: 5274 checks, 574 files.
* Browser e2e (Playwright + Chromium): wasm shell 21/21 checks
  (charts/SGR, char-mode auto-switch, readline editing + undo +
  history, select with filter, multiselect/confirm, full-screen app,
  dashboard live repaint + NAWS resize relayout); websocket client
  against the native driver 13/13 twice on one instance (plus an
  ascii-subprotocol multi-window burst) -- the flow that caught the lws
  wedge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UvX1HbmGk9zWGBsW4Rkcq
2026-07-14 09:53:26 -05:00
Yucong Sun
cc2c2c2b56
docs: capture third-party vendoring and autogen-pin lessons in AGENTS.md / README (#1264)
New AGENTS.md section 14 documents the src/thirdparty update workflow
learned during the 2026-07 full-fleet upgrade: vendor byte-exact from a
git clone of the upstream tag (never through a lossy fetch channel --
passing tests do not prove a vendored tree is faithful), hunt for and
re-apply the FluffOS-local patches (current inventory listed), prune
policy with the known unconditional-add_subdirectory traps, per-platform
validation gotchas (clang 18 configure probes, musl vs glibc transitive
includes, Windows STATUS_DLL_NOT_FOUND from VS-only runtime DLLs), the
lws "system vhost" adoption fix, and the fact that no test suite covers
a websocket client connection (smoke-test procedure included).

Section 5 and the README also document the new autogen pin machinery:
committed bison/flex outputs pin their generator versions and carry an
input-hash stamp, so older hosts build from the committed copies and
same-version-different-patch generators can no longer churn them.


Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-13 20:07:59 -05:00
Yucong Sun
897821731f
build: stop autogen churn -- pin generator versions, gate copy-back on input hash (#1263)
Every local build regenerated grammar.autogen.cc/.h and lexer.autogen.cc
and copied them back into the source tree, churning git whenever the
host's bison/flex emitted cosmetically different output than whatever
produced the committed copy. The existing normalization only tokenized
paths and header guards; it could not help when two generators that even
REPORT the same version (3.8.2) differ in emitted code (the committed
grammar had a distro-patched bison's "int yynerrs YY_ATTRIBUTE_UNUSED;"
that vanilla 3.8.2 downgrades on every rebuild).

Two-layer fix:
- Version pin: the committed generated files record the generator that
  produced them ("made by GNU Bison X.Y.Z", YY_FLEX_*_VERSION). A host
  with an OLDER generator no longer regenerates at all -- it builds from
  the committed copy via the existing pre-generated path. Hosts at/above
  the pin still regenerate, so CI keeps validating grammar.y/lexer.l
  against a real toolchain.
- Input-hash gate: the post-build copy-back stamps the committed file
  with the sha256 of its INPUT (grammar.y / lexer.l) and is skipped
  entirely while that hash is unchanged. Generator differences alone can
  no longer touch the committed files; editing the .y/.l still updates
  them exactly as before.

Also: the copy-back now skips writing byte-identical content, so it no
longer bumps mtimes and cascades pointless relinks on every build.

The three committed autogen files are re-stamped in this commit (one-time
churn) as the new pinned baseline: bison 3.8.2 / flex 2.6.4, matching
every CI image.


Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-13 19:18:26 -05:00
Claude
5ac4e8c0e6 thirdparty: fix libwebsockets configure probes under clang 18+
All 23 CHECK_C_SOURCE_COMPILES header probes in the vendored lws 4.5.8
CMakeLists.txt spell their test program "void main(void)" -- invalid C
that clang 18+ (and current AppleClang) rejects outright with "'main'
must return 'int'". Every probe then silently reports Failed, including
LWS_HAVE_PTHREAD_H, whose absence leaves lws_mutex_t/lws_tid_t undefined
and breaks the whole build -- master's CI run failed on all four Ubuntu
clang jobs and both macOS jobs this way. Changed to "int main(void)"
(still unfixed upstream on the v4.5-stable branch). Reproduced and
verified locally with a full clang 18 configure + build + test cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 19:17:04 -04:00
Claude
c9820c6b08 thirdparty: fix backward-cpp v1.6 on musl and Windows
Follow-up fixes for the backward-cpp v1.6 update that master's CI run
surfaced (Alpine/static, Docker, and both Windows jobs failing):

- backward.hpp: include <dlfcn.h> under the BACKWARD_HAS_DW branch (and
  the backtrace branch) -- v1.6's TraceResolverLinuxBase uses Dl_info
  unconditionally but only includes dlfcn.h on the BFD/DWARF branches; on
  musl (Alpine / the Docker image build) nothing pulls it in transitively
  and the DW-only build fails with "'Dl_info' has not been declared".
  This restores a local patch the old vendored copy carried.
- BackwardConfig.cmake: link dbghelp/psapi on WIN32 -- v1.6 ships a real
  Windows StackWalk64/dbghelp implementation the old vendored copy
  predated, so the MSYS2/MinGW64 driver link failed with undefined
  references to SymFromAddr/StackWalk64 et al. Deliberately WITHOUT
  upstream's companion MinGW msvcr90 link: msvcr90(d).dll only exists on
  machines with Visual Studio installed, so linking it makes every
  produced binary die at load with STATUS_DLL_NOT_FOUND; the lone
  _set_abort_behavior() call it served is now guarded to _MSC_VER in
  backward.hpp instead (MinGW keeps the default abort() behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 19:17:04 -04:00
Yucong Sun
c74ccc8523
wasm: add crash/error debug modal to the default page (#1261)
Surfaces load failures, WASM runtime aborts, uncaught errors, and
unhandled promise rejections in a modal with the message, stack trace,
and environment details, plus copy/reload actions -- previously these
only appeared as a truncated one-line status string in the header.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-13 17:29:52 -04:00
Claude
1575e60234 thirdparty: prune widecharwidth test scaffolding
Drop widecharwidth's test.cpp and its Makefile (whose only targets built
that test against an out-of-repo wcwidth9.h). The consumed header
widechar_width.h and the generate.py regeneration tool stay; nothing in
the build or CI invoked either removed file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
f35d4a71f3 thirdparty: prune unused libevent test/sample trees
Drop libevent's test/ and sample/ directories -- the FluffOS build forces
EVENT__DISABLE_TESTS, EVENT__DISABLE_REGRESS, and EVENT__DISABLE_SAMPLES
ON, so nothing references them. The library itself is unchanged; a
version upgrade was explicitly excluded from this round per maintainer
request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
2d303af0b0 thirdparty: prune unused libtelnet doc/test/util trees
Drop libtelnet's doc/, test/, and util/ directories -- their
add_subdirectory calls are commented out in its CMakeLists.txt and
nothing in the FluffOS build references them. The library itself
(libtelnet.c/.h) is unchanged; a version upgrade was explicitly excluded
from this round per maintainer request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
8fe05a5d40 thirdparty: update libwebsockets to 4.5.8
Replace the vendored libwebsockets tree (a v4.2-stable snapshot) with a
byte-exact copy of the v4.5.8 tag. Picks up the CVE-2025-1866 fix
(out-of-bounds pointer arithmetic, reachable when extensions are enabled,
as they are in this build) plus four years of upstream fixes. The three
FluffOS-local patches on the old tree are all obsolete upstream, so the
vendor tree now carries no local modifications. Unused minimal-examples*,
test-apps/, contrib/, doc-assets/, and plugin-standalone/ trees are
dropped (all behind disabled options or unreferenced); plugins/ and
lwsws/ stay (unconditional add_subdirectory, internally guarded), as do
win32port/ (used by the WIN32 build) and scripts/ (referenced from cmake).

Integration porting:
- src/CMakeLists.txt: force OFF new default-ON subsystems the driver does
  not use -- EVLIB_PLUGINS (the libevent event lib must be compiled in
  statically), HTTP_DIGEST_AUTH (force-implies GENCRYPTO, which hits an
  #error guard against deprecated EC APIs on OpenSSL 3.0+), LHP/UPNG/
  SECURE_STREAMS (embedded display-list parsers; UPNG carries
  CVE-2025-11679), and MINIMAL_EXAMPLES (pruned from the vendor tree);
  dedupe a repeated LWS_WITHOUT_TESTAPPS line.
- src/net/websocket.cc: adopt incoming sockets onto our own vhost via
  lws_get_vhost_by_name()/lws_adopt_socket_vhost() -- since v4.3 the
  context's vhost list is headed by an internal "system" vhost carrying
  none of our ws protocols, so lws_adopt_socket() (which uses the list
  head) rejected every websocket upgrade with "No supported protocol".
  Also switch the lws_http_mount initializer from positional to
  field-by-field init (upstream inserted a bitfield mid-struct, silently
  shifting positional initializers).
- src/net/transport_libevent.cc: fail closed when the websocket context
  cannot be created at boot (previously the listener kept accepting and
  the first connection null-derefed inside lws), and close the accepted
  fd if adoption fails instead of leaking it.

Verified: clean build, 312/312 GTest, LPC testsuite green, and an
end-to-end websocket smoke test (101 upgrade + first data frame) on all
four paths: plain and TLS ports with ascii, telnet, and binary
subprotocols.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
9cc0555ff3 thirdparty: update utfcpp to 4.1.1
Update the vendored nemtrif/utfcpp from v3.2.1 to v4.1.1, byte-exact from
the upstream tag. Fixes an out-of-bounds read in unchecked::utf16to8() on
UTF-16 input ending in a truncated lead surrogate (upstream #78; not
currently called by the driver but shipped in-tree), adds char8_t/
u8string C++20 support (new source/utf8/cpp20.h). The v4 renames of the
public utf8::uint*_t aliases do not affect the driver's single call site
(utf8::replace_invalid in strutils.cc). Unused tests/ and bench/ trees
are dropped (both behind off-by-default options).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
3e2eb0c888 thirdparty: update fmt to 12.2.0
Update the vendored fmtlib/fmt from 10.1.0 to 12.2.0, byte-exact from the
upstream tag. A two-major-version jump: core.h is now a thin wrapper over
the new base.h, big float-formatting speedups, many new std formatters,
and several hardening fixes directly relevant here (out-of-bounds reads
in printf-style formatting, a buffer overflow in format-string
compilation/debug format, compile-time format checks no longer silently
truncating at an embedded NUL) -- the driver funnels mudlib- and
network-derived text through fmt-based formatting in its diagnostic
paths. Unused doc/, test/, and non-cmake support/ tooling are dropped
(support/cmake stays; it is referenced unconditionally).

Integration port: fmt moved the range/iterator overloads of fmt::join to
fmt/ranges.h, so compiler_utils.cc and external.cc now include it. All
other call sites (fmt::format/FMT_STRING/to_string) compile unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
b3ddc42bd5 thirdparty: update json to 3.12.0
Update the vendored nlohmann/json from 3.11.2 to v3.12.0, byte-exact from
the upstream tag (include/ multi-header tree, the single_include
amalgamated header, LICENSE.MIT, meson.build). Rolls up 3.11.3 + 3.12.0:
fixes a segfault in deeply-nested scenarios, an exception-path memory
leak, parse(nullptr) crash, GCC 13 build failures; adds
JSON_DIAGNOSTIC_POSITIONS byte-offset diagnostics, std::optional support,
templated NLOHMANN_DEFINE_TYPE_* macros. No FluffOS code touches the
changed internals, so no porting was needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
94f293413d thirdparty: update filesystem to v1.5.14
Update the vendored gulrak/filesystem from v1.5.12 to v1.5.14, byte-exact
from the upstream tag. Bug-fix release: Windows directory_iterator no
longer misidentifies files after a symlink entry, stem()/filename()/
extension() fixed on POSIX for names containing colons, Y2038-safe
FILETIME conversion, copy_file() honors skip_existing and preserves
permissions, EINTR retry on network filesystems. No API changes; no
FluffOS code touched. Unused test/ and examples/ trees are dropped
(guarded by off-by-default options).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
680fa60396 thirdparty: update backward-cpp to v1.6
Update the vendored bombela/backward-cpp to the v1.6 tag: backward.hpp
and backward.cpp are byte-exact upstream (inode-based stale-binary guard
in the libbfd resolver, Windows FormatMessageA/LocalFree leak fix,
cfile_streambuf::overflow fix, TraceResolverImplBase refactor). Two
FluffOS-local build patches are preserved: BackwardConfig.cmake (links
elf/dl/lzma/bz2/zstd alongside dw -- required for elfutils on modern
Ubuntu -- and keeps add_backward() PUBLIC so the driver executable
inherits the libs through the static libdriver) and the CMakeLists.txt
cmake_minimum_required bump. Unused test/, test_package/, and doc/ trees
are dropped (BACKWARD_TESTS defaults OFF as a subproject).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00
Claude
ae936d21ab thirdparty: update scope_guard to 0.9.4
Update the vendored Neargye/scope_guard header from 0.9.0 to 0.9.4,
byte-identical to the upstream tag (verified by blob hash). Internal
macros renamed with a NEARGYE_SCOPE_GUARD_ prefix, MSVC exception
detection switched from _HAS_EXCEPTIONS to _CPPUNWIND, new rvalue
static_asserts. All FluffOS call sites use the DEFER {...} macro (always
an inline rvalue lambda), so no changes were needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCUi8QKaFxeTyLXMeVkqn4
2026-07-13 11:34:03 -04:00