You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stop a long-lived process growing by the types and regexes it has read (Covered by dco/Brian_Egge.md)
Follow-up to morganstanley#549 and morganstanley#550, which bounded the fuzz harnesses against two
kinds of growth and left the library's own behaviour alone. This is the
library half.
1. The type memo is compacted where untrusted type descriptions are decoded.
hobbes::decode interns every type it builds in the process-wide type memo
(tctorMaps in lang/type.C), which holds a reference of its own to each
entry and lets go only when compactMTypeMemory() is called -- which, until
now, happened in exactly one place (eval/search.C). The RPC server
(ipc/net.C) decodes peer-supplied type descriptions and serialized
expressions on every prepare request, and the machine REPL (ipc/prepl.C)
decodes the same from its driver, and neither compacted, so a server handed
many distinct type descriptions grew by every one -- about 380 bytes per
distinct type, for the life of the process.
Compact after each request that decodes: net.C's prepare cases (0 and 1,
including when they fail, since a rejected description is the one that is
only garbage) and prepl.C's meta commands. What the request needed is held
by the compiler and stays; what it only passed through goes. The hot paths
-- invoking a prepared expression, invoking a compiled thunk -- decode
nothing and are left alone. A compaction walks the memo under its lock and
costs on the order of a decode, against a request that compiles an
expression.
unique_refc_map::get now returns its entry by value rather than by reference
into the map. The reference was handed out after the lock was released, and
compact() can erase that entry from another thread the moment it is -- a
latent hazard that periodic compaction from a server thread would turn into
a real one. The one caller (MonoType::makeType) copied the result
immediately anyway.
2. A compiled regex matcher is reused for the same regexes.
A regex literal is compiled into a matching function where it is read:
makeRegexFn determinizes it and defines the result under a fresh name
(".regex." + freshName()), and nothing removes that. morganstanley#550 measured about
83KB retained in the compiler per read of a twenty character regex. A
process that reads the same regex literals repeatedly -- the same expression
text arriving over RPC, a REPL session -- compiled a new matcher each time.
makeRegexFn now keeps, per compiler, the matchers it has compiled, by an
encoding of the regexes each one matches, and a match on the same regexes
reuses the function and result mapping compiled for them: the function is
pure in its input and the mapping is a property of the regexes, so neither
depends on where the match that uses them is. The capture buffer expression
and binding names are remade per call, because they are cheap and carry a
source location. The encoding is injective by construction -- every node
tagged, every name length-prefixed -- where show() is not: it prints
characters raw, so 'a|b' (either) and 'a\|b' (three literals) print alike
and must not share a matcher. Distinct regexes still each get a matcher;
that is compiling code, and is not a leak.
Measured as morganstanley#550 did, one compiler kept alive, cycles of 100 reads of the
OSS-Fuzz testcase's regex with the memo compacted between cycles:
before (morganstanley#550's measurement) +17.1MB per 100 reads, +8.3MB net of compaction
after +0.47MB per 100 reads, +0.45MB net of compaction
The remainder is the syntax-error path of the parser retaining its source
text, which is documented in read/parser.C and is not touched here.
The full test suite passes. test/Matching.C checks that a reused matcher
matches as the first did, that the two regexes above do not share one, and
that captured groups still bind through a reused matcher.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016rGT4C394qeh2DBQhqy3Tb
0 commit comments