Both found by UBSan during a sanitizer smoke run (#1440). Neither produces
a wrong answer today; both are the kind of UB that stays invisible until a
compiler version or optimisation level changes.
timeutil.cpp:884 -- left shift into the sign bit
left shift of 1095782985 by 8 places cannot be represented in type 'int'
ParseThreeLetters accumulates a token a byte at a time into an int, and the
loop runs over the WHOLE token before the three-letter check below it. A
four-letter alpha token therefore overflows: 1095782985 is four ASCII bytes.
Accumulate in uint32_t instead.
No accepted input changes value. A three-letter token occupies 24 bits, and
anything longer is rejected by `q - p != 3` before iHash is ever stored --
the overflowed value could never reach the MonthTabHash comparison.
dbt_emit_a64.h:156 -- shift by the full width
shift exponent 64 is too large for 64-bit type 'uint64_t'
The logical-immediate encoder rotates an element with
((elem >> rot) | (elem << (size - rot))) & mask
and size reaches 64, so rot == 0 is elem << 64. AArch64 masks the shift
count to six bits, so it silently becomes elem << 0 and the correct answer
falls out by accident -- on the very architecture this emits code for.
rot == 0 is the identity rotation, so spelling that case out removes the UB.
For size < 64 the shift was already defined and the mask discarded it, so
the special case is the same value at every size.
Verified the encoder is unchanged rather than assuming it: enumerated the
(size, elem, rot) space and compared the old form -- evaluated the way
AArch64 actually behaves, with the shift count masked to six bits -- against
the new one.
checked=34867400 differences=0
make test-dbt all green, including 960 hand-assembled and both ELF legs
smoke 1497 passed, 0 failed, 315/315
UBSan timeutil and dbt_emit_a64 reports gone
With #1459 and #1462 this takes the tree to zero standing UBSan reports,
which is what makes a future run signal rather than noise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
After #1374/#1384 the parser is 64-bit, but several sites still stored
the result in long. On LLP64 that truncates again: shl(1,4294967296)
became a no-op shift, and printf(%d,4294967296) printed 0.
Widen the residual sinks to int64_t (funmath fast paths, shl/shr count,
printf %d/%i, width check, netaddr threshold, timeutil, mathutil digits)
and add smoke coverage for the two softcode-visible failure modes.
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>
connected_at, last_time, and the server start time are int64 Unix-second
counts, but dump_restart_db/load_restart_db round-tripped them through the
32-bit putref/getref channel (dbref-sized I32BUF_SIZE). The value fits today,
but truncates after 2038-01-19 (Y2038), wrapping the WHO "On For" column to
nonsense for any session that spans an @restart.
Add putref64/getref64 (mux_i64toa/mux_atoi64 based) and bump restart.db to
version 5, reading v<=4 the old 32-bit way. Covers both the bsd and GANL
backends (GANL uses net.cpp's dump/load_restart_db).
Also fix GetUTCLinearTime's time() fallback (the no-HAVE_GETTIMEOFDAY branch),
which omitted the EPOCH_OFFSET that the gettimeofday branch adds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
idiv(-9223372036854775808,-1) and remainder(...) crashed the whole server:
INT64_MIN / -1 overflows and traps (#DE/SIGFPE) on x86. Reproduced live from a
Wizard `think` — any connected player could kill netmux.
i64Mod and the out-of-line i64Remainder already guarded the INT_MIN/-1 case,
but i64Division and i64FloorDivision did not, and all four inline header
variants did raw x/y or x%y. Guard the overflow in every definition so it is
correct under either SMALLEST_INT_GTE_NEG_QUOTIENT branch and regardless of
which inline/out-of-line copy the linker selects:
- timeutil.h: the four inline i64Division/i64Remainder/i64Mod/i64FloorDivision
now return INT64_MIN (division) / 0 (remainder) for the INT_MIN/-1 case.
- timeutil.cpp: i64Division and i64FloorDivision gain the same guard (i64Mod /
i64Remainder already had it).
- hir_opt.cpp: the HIR_DIV / HIR_REM constant folds used raw C++ / and %
(guarded only by !=0); add the INT_MIN/-1 guard so post-optimization-constant
operands (nested arithmetic) fold to INT_MIN / 0 instead of trapping.
Reachable from the interpreter (fun_idiv -> i64Division; remainder ->
i64Remainder), the JIT try_fold (i64Division), and the hir_opt folds.
Verified live (build clean, smoke 1115/1115): the exact repro now returns
-9223372036854775808; mod/remainder return 0; and the runtime paths
(interpreter, attribute reads, JIT-compiled u() with stack-arg divisors) all
handle div-by-zero (#-1 DIVIDE BY ZERO) and INT_MIN/-1 without crashing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three defensive fixes from the same review pass:
- dbt_register_intrinsic() silently dropped registrations past
MAX_INTRINSICS in all three backends -- the root cause of #778, which
surfaced as "all float math returns empty" and took a multi-cause
hunt to diagnose. The registration count is fixed at build time
(pretranslate_tier2), so overflow is a developer error: fail loudly
at startup with fprintf+abort instead of miscompiling every affected
function.
- do_convtime()'s year-range guard (#715) parsed with mux_atol(),
which accumulates without overflow detection: a long-enough year
string wraps the long and can land back inside the valid range
(e.g. 2^32+2000 -> 2000 where long is 32-bit, as in the dowin32.sh
builds). Cap the digit count at 5 before parsing -- valid years
have at most 5 digits. convtime(Sat Jan 01 12:00:00 4294969296)
now returns #-1 INVALID DATE.
- mux_IGameEngine::DbConvert() gained bForce in ecae4df9d without an
interface-ID bump, so a stale engine.so paired with a new driver
would pass QueryInterface and silently read garbage for bForce.
Bump IID_IGameEngine ...C9D1 -> ...C9D2 so the mismatch fails
QueryInterface instead.
Verified: full smoke 1102/0/0 under --enable-jit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds adversarial/boundary coverage for the date scanner behind ParseDate(),
do_convtime(), and ParseFractionalSecondsString() to tests/libmux/, hitting
the overflow / narrowing / truncation corners the .mux smoke tests
(convtime_fn, parsedate_fn) don't reach.
Writing the harness surfaced a real bug, fixed here:
do_convtime() narrowed the year with `ft->iYear = (short)mux_atol(p)` and no
range check, so "Sat Jun 7 12:34:56 9999999999" wrapped to (short)-7169 --
in the accepted [-27256, 30826] range -- and was returned as a valid (wrong)
date. This is the same class as #708, which guarded ParseDate() but missed
the do_convtime() conversion branch. Validate the full year against the
timeutil range before narrowing, mirroring the ParseDate() guard.
Harness (17 new cases, all via the already-built libmux.so):
- ParseDate: valid ISO date/datetime/zone-flag; empty/garbage rejected;
10-digit overflow rejected (#707); out-of-range year rejected before the
short narrowing (#708, e.g. 67536 -> 2000); exact accepted-year boundaries
(30826 ok / 30827 reject / -27256 ok / -27257 reject); truncated ISO;
invalid calendar components (month 13, June 31, Feb 29 in a non-leap year);
invalid time-of-day; ordinal day-of-year.
- do_convtime: happy path; the year-overflow rejection above; bad day/time.
- ParseFractionalSecondsString: scaling, signs, leading/trailing dot,
garbage rejection.
The harness is fail-closed: verified by reverting the do_convtime fix and
confirming test_convtime_year_overflow_rejected fails (45/1), then passing
(46/0) with the fix.
Makefile: the test target now prepends LIBDIR to LD_LIBRARY_PATH so it loads
the freshly-built mux/lib/libmux.so rather than a stale copy an ambient
LD_LIBRARY_PATH (e.g. ~/lib) would otherwise shadow -- DT_RUNPATH from -rpath
loses to LD_LIBRARY_PATH. Added a .gitignore for the build artifacts.
Smoke (--enable-jit): 1086 / 0 failed / 0 crashes; all convtime/parsedate
cases pass. Harness: 46 / 0.
Closes#715.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>