Finding 6 of the 2026-07-29 security review: messages became data
structures before anyone checked them. read never executes code, but
it can be made to yield self-referential structure (which hangs
ordinary list walks, plist-get included), objects that impersonate
internal record types (#s syntax yields real hash tables and records),
and any number of fresh symbols.
cg-net--valid-p accepts what is recognised: a proper, even-length
plist whose :type is in the per-peer whitelist -- the host accepts
(hello move), a client accepts (welcome state full) -- built only from
conses, vectors, strings, numbers and symbols, with no shared or
circular structure, at most cg-net-max-nodes nodes. The check runs in
cg-net--filter before the Finding 2 scrub, so nothing cyclic or opaque
reaches scrub, handlers, or game code. The cycle-tolerance of the
scrub walker remains as a second belt.
Named leftovers (in PATCHES.md): symbols are interned by read before
validation can see them, bounded only by Finding 4 line cap; and shape
is not semantics -- a hostile host can still send absurd but
well-shaped state, which is the reserved trust-model question.
Test cgt-net-shape-gate fails against the previous code with the
receipt (equal (5 (1 2 1 2 . #2) #s(hash-table)) (5)) -- the cycle and
the hash table both reached cg-net-apply-move. cgt-net-valid-p pins
the unit contract (void-function before the patch).
Finding 5 of the 2026-07-29 security review: nothing watched for a
connection ending, so players who left stayed on the client list
forever and the host kept trying to send to them.
cg-net--host-sentinel removes a closed connection from the list; it is
installed on every accepted connection. This also tidies the drops
made by the Finding 4 bounds, which delete-process over-limit peers.
Seat numbers are still not reused after a departure -- deliberate, and
recorded in the sentinel docstring: a stale seat must not be inherited
by a stranger mid-game. Named in the handback as remaining behavior.
Test cgt-net-reaps-disconnected fails against the previous code: after
the client disconnected the list still held it (= 0 1).
Finding 4 of the 2026-07-29 security review: incoming data was
collected until a newline arrived with no limit on how much, so a
connection sending forever without a newline grew the buffer until
memory ran out; the client list could grow the same way. No login was
needed (Finding 1).
Two bounds, both defcustoms: cg-net-max-line (64 KiB -- generous for a
card game) closes any connection whose pending newline-less data
exceeds it, and cg-net-max-connections (8) closes connections arriving
past the limit before they are seated. Finding 1's loopback default
narrows who can reach the port; this bounds what anyone who does reach
it can consume -- a mitigation that depends on another setting staying
at its default is not a bound.
Tests cgt-net-line-cap and cgt-net-connection-cap fail against the
previous code: the 80 KiB flooder stayed connected, and a third
connection stayed live past a cap of two (= 2 3).
Finding 1 of the 2026-07-29 security review: hosting bound 0.0.0.0 --
every interface -- with no password and no encryption, so anyone who
could reach the machine's port was handed a seat and the game state.
New defcustom cg-net-host-address defaults to 127.0.0.1; the wide-open
value remains available and its docstring says exactly what choosing it
means. This converts "anyone who can reach you" into "someone you
deliberately let in", and bounds Finding 4's unauthenticated
memory-exhaustion route to local callers along the way.
Test cgt-net-host-loopback-default checks both the default value and
the actual bound address (process-contact :local). Against the
previous code it fails with (void-variable cg-net-host-address); the
old bind, captured before the patch: [0 0 0 0 PORT].
Finding 2 of the 2026-07-29 security review (CONFIRMED there): text
properties survive the prin1/read round trip the protocol is built on,
so a malicious host could send a client strings whose properties rebind
keys or carry expressions evaluated during redisplay.
New cg-net--scrub walks a decoded message and passes every string
through substring-no-properties; conses and vectors are copied, shared
and circular structure is tolerated. It is applied inside
cg-net--filter -- the one decode point both the host and the client
read through -- so both directions are covered at the boundary rather
than at each use site.
Test cgt-net-strips-properties (loopback, both directions) fails
against the previous code with properties intact: value (keymap (keymap)).
Bump z per the releasable-set convention. Ship the Info manual
(make info; make info-emacs fallback). Generate README.md from
README.org via Emacs batch ox-md, with a pre-commit hook and
board screenshots.
Full-SVG 500 made mouse-operable for newcomers: kitty Discard button and
five-card cap, on-table phase banner, ? Help/Rules overlay with the bid
legend, legal-play dimming, card-size slider, and a layout pass that
moves the Help and size controls into the log panel so nothing overlaps.
Bump all files to 1.0.90, add NEWS, a README testing quick-start, and
make the shared engine files checkdoc-clean.
Host-authoritative networked 500: host sits South, joiners take W/N/E,
open seats filled by AI. Per-seat rotated state hides other hands and the
kitty; clients reuse the single-player renderer/commands via :around advice,
so cg-bid.el/cg-bid-ui.el are untouched. Adds cg-bid-host/cg-bid-join, a
start-now lobby with auto-start at four players, and cg-bid-shuffle-partners.
cg-net.el gains cg-net-connect-functions. Verified: clean byte-compile,
checkdoc, 34/34 ERT (incl. 3 new net tests) and a two-process TCP game.