Skip to content

Commit 157eb72

Browse files
brianeggeclaude
andcommitted
Do not hold a reference into the type memo across constructing a type (Covered by dco/Brian_Egge.md)
The previous commit rewrote unique_refc_map::get to return by value, and in doing so held a reference into the map across mk(), the call that constructs the type being interned. Constructing a type can intern the types it is made of -- FixedArray and Array normalise their element type on the way in, for one -- which re-enters the same map (the mutex is recursive for exactly this) and can insert, and an insertion can rehash, and a rehash moves the entry the reference pointed at. The write through it afterwards is a write to freed memory. CI caught it: hobbes-test died with "double free or corruption" in Compiler/ccInManyThreads on the clang-16 build, and only there, as use-after-free does. The original code looked the key up again after mk() for this reason, and that is restored in a form that also keeps the first object if mk() interned this very key on the way: look up, construct, look up again, keep whichever entry is there. While here, two of Copilot's review comments on morganstanley#552: the compaction in net.C and prepl.C ran after the work and so was skipped when the work threw -- and the failing input is the one that interned nothing but garbage. Both sites now use a scope guard (CompactMTypeMemoryAtExit, in lang/type.H) declared at the top of the request, so it runs on every way out: success, the decode throwing, or the error write itself throwing on a dead peer. And the reuse test in test/Matching.C now proves reuse rather than just its correctness, by counting the ".regex." functions defined in a fresh compiler as the same and a look-alike regex are compiled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016rGT4C394qeh2DBQhqy3Tb
1 parent 64152c1 commit 157eb72

5 files changed

Lines changed: 74 additions & 19 deletions

File tree

include/hobbes/lang/type.H

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,20 @@ inline MonoTypePtr fnresult(const MonoTypePtr& fty) {
12601260

12611261
void compactMTypeMemory();
12621262

1263+
// compact the type memo when the enclosing scope ends, however it ends: for
1264+
// code that decodes types it may not keep -- a request handler, say -- so that
1265+
// the types a rejected input interned are released on the failure path as
1266+
// well as on success
1267+
class CompactMTypeMemoryAtExit {
1268+
public:
1269+
explicit CompactMTypeMemoryAtExit(bool armed = true) : armed(armed) { }
1270+
~CompactMTypeMemoryAtExit() { if (this->armed) { compactMTypeMemory(); } }
1271+
CompactMTypeMemoryAtExit(const CompactMTypeMemoryAtExit&) = delete;
1272+
CompactMTypeMemoryAtExit& operator=(const CompactMTypeMemoryAtExit&) = delete;
1273+
private:
1274+
bool armed;
1275+
};
1276+
12631277
// shorthand for making file ref types
12641278
MonoTypePtr fileRefTy(const MonoTypePtr&, const MonoTypePtr&);
12651279
MonoTypePtr fileRefTy(const MonoTypePtr&);

include/hobbes/util/ptr.H

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,23 @@ template <typename T, typename ... Args>
2626
std::shared_ptr<T> get(const std::function<T*(Args...)>& mk, const Args&... args) {
2727
std::lock_guard<std::recursive_mutex> lock(mutex);
2828
auto k = std::tuple<Args...>(args...);
29-
auto& r = this->values[k];
30-
if (!r) {
31-
r = std::shared_ptr<T>(mk(args...));
29+
30+
auto found = this->values.find(k);
31+
if (found != this->values.end() && found->second) {
32+
return found->second;
33+
}
34+
35+
// no reference into the map is held across mk(): constructing a type can
36+
// intern the types it is made of, re-entering this map (the mutex is
37+
// recursive for that reason), and an insertion there can rehash it. So
38+
// construct first, then look the key up again, and keep whichever entry
39+
// is there -- mk() may have interned this very key on the way.
40+
std::shared_ptr<T> made(mk(args...));
41+
std::shared_ptr<T>& slot = this->values[k];
42+
if (!slot) {
43+
slot = made;
3244
}
33-
return r;
45+
return slot;
3446
}
3547

3648
size_t compact() {

lib/hobbes/ipc/net.C

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ void evaluateNetREPLRequest(int c, void *d) {
234234
case 0:
235235
// prepare a lexical expression with input and output types given
236236
try {
237+
// the type descriptions read below are interned in the process-wide
238+
// type memo whether or not they are accepted; release what this
239+
// request does not end up keeping, on every way out of here
240+
CompactMTypeMemoryAtExit compactAfter;
241+
237242
exprid eid = 0;
238243
fdread(c, &eid);
239244

@@ -259,11 +264,12 @@ void evaluateNetREPLRequest(int c, void *d) {
259264
fdwrite(c, uint8_t(0));
260265
fdwrite(c, std::string(ex.what()));
261266
}
262-
compactMTypeMemory();
263267
break;
264268
case 1:
265269
// prepare a serialized expression, also return its type
266270
try {
271+
CompactMTypeMemoryAtExit compactAfter;
272+
267273
exprid eid = 0;
268274
fdread(c, &eid);
269275
RawData exprd;
@@ -287,7 +293,6 @@ void evaluateNetREPLRequest(int c, void *d) {
287293
fdwrite(c, uint8_t(0));
288294
fdwrite(c, std::string(ex.what()));
289295
}
290-
compactMTypeMemory();
291296
break;
292297
case 2:
293298
// invoke a prepared expression

lib/hobbes/ipc/prepl.C

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,16 @@ void runMachineREPLStep(cc* c) {
315315
int cmd = 0;
316316
fdread(STDIN_FILENO, &cmd);
317317

318+
// the meta commands below decode types and expressions sent by the peer,
319+
// and every type that builds is interned in the process-wide type memo,
320+
// which holds on to it until asked not to. Ask once this step is over,
321+
// however it ends: what the command needed is held by the compiler and
322+
// stays, what it only passed through -- including everything a rejected
323+
// input interned before it was rejected -- goes. Invoking a compiled
324+
// function (the default case) decodes nothing and is the hot path, so it
325+
// is left alone.
326+
CompactMTypeMemoryAtExit compactAfter(cmd < CMD_COUNT);
327+
318328
switch (cmd) {
319329
case CMD_REFINE_VNAME: {
320330
// legacy method to refine the type of a variable given an initial type "guess"
@@ -508,16 +518,6 @@ void runMachineREPLStep(cc* c) {
508518
resetMemoryPool();
509519
break;
510520
}
511-
512-
// the meta commands above decode types and expressions sent by the peer,
513-
// and every type that builds is interned in the process-wide type memo,
514-
// which holds on to it until asked not to. Ask after each one: what the
515-
// command needed is held by the compiler and stays, what it only passed
516-
// through goes. Invoking a compiled function (the default case) decodes
517-
// nothing and is the hot path, so it is left alone.
518-
if (cmd < CMD_COUNT) {
519-
compactMTypeMemory();
520-
}
521521
} catch (std::exception& ex) {
522522
std::string exn = ex.what();
523523
dbglog("*** " + exn);

test/Matching.C

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,13 +522,37 @@ TEST(Matching, hugeRegexDFACompilesWithoutQuadraticBlowup) {
522522
// first time rather than defining another. Reuse has to be by the regex
523523
// itself, not by how it prints: 'a|b' (either) and 'a\|b' (three literals)
524524
// print alike but match differently, and must not share a matcher.
525+
// a compiled matcher is a function defined in the compiler under a ".regex."
526+
// name, so counting those in its type environment counts the matchers it holds
527+
// (dumpTypeEnv hides dot-prefixed names, so go to the environment directly)
528+
static size_t regexMatchersDefinedIn(cc &x) {
529+
size_t n = 0;
530+
for (const auto &s : x.typeEnv()->boundVariables()) {
531+
if (s.rfind(".regex.", 0) == 0) {
532+
++n;
533+
}
534+
}
535+
return n;
536+
}
537+
525538
TEST(Matching, regexMatchersAreReusedButOnlyForTheSameRegex) {
526-
auto either1 = c().compileFn<int(const std::string &)>(
539+
// a fresh compiler, so that the count of matchers is this test's to reason about
540+
cc lc;
541+
const size_t before = regexMatchersDefinedIn(lc);
542+
543+
auto either1 = lc.compileFn<int(const std::string &)>(
527544
"s", "match s with | 'a|b' -> 1 | _ -> 0");
528-
auto either2 = c().compileFn<int(const std::string &)>(
545+
EXPECT_EQ(regexMatchersDefinedIn(lc), before + 1);
546+
547+
// the same regex again: no new matcher
548+
auto either2 = lc.compileFn<int(const std::string &)>(
529549
"s", "match s with | 'a|b' -> 2 | _ -> 0");
530-
auto literal = c().compileFn<int(const std::string &)>(
550+
EXPECT_EQ(regexMatchersDefinedIn(lc), before + 1);
551+
552+
// a regex that merely prints the same: a new one
553+
auto literal = lc.compileFn<int(const std::string &)>(
531554
"s", "match s with | 'a\\|b' -> 3 | _ -> 0");
555+
EXPECT_EQ(regexMatchersDefinedIn(lc), before + 2);
532556

533557
EXPECT_EQ(either1("a"), 1);
534558
EXPECT_EQ(either1("b"), 1);

0 commit comments

Comments
 (0)