We run CosmicIRC, a small independent IRC network, on a handful of eggdrop bots written in Tcl. One of them runs a "Typo League" script — it watches the channel, flags misspelled words, and keeps a leaderboard. Harmless, fun, the kind of feature nobody thinks twice about.
It also turned out to be really good at crashing eggdrop, and not in a way that made any sense.
The first crash, and the wrong lesson
The first version of the script (on a bot we'll call Pulsar) used aspell as a coprocess: shell out to aspell -a --lang=en, hold a pipe open, and check words against it as they came in. It also had an AI-generated "roast" feature on join, using the same async-HTTP pattern as another script we'd already shipped without issues.
Pulsar started crashing a few minutes into any session with real traffic — exitCode=139 in docker events, which is SIGSEGV. Nothing in the eggdrop log. No stack trace. Just gone.
We root-caused a crash here, but it wasn't the one we thought we were chasing. The Dockerfile that built this bot's image had zero version pins — one apt-get install line, whatever the package cache happened to have that day. Adding aspell/aspell-en to that line invalidated the Docker layer cache, which forced a fresh apt-get update, which pulled different (untested) versions of everything else on that line too — including tcl-tls. That's a real bug, and pinning every package fixed that crash. (We wrote it up separately, because it deserves its own post: pin your Dockerfile.)
With the segfault gone, we re-enabled the typo script. It crashed again — same docker events signature, but a different error: free(): invalid size. That's not a segfault. That's heap corruption — something wrote past the end of an allocation, and the crash shows up later, somewhere unrelated, when the allocator notices its bookkeeping is wrong. Much nastier to trace, because the code that crashes is rarely the code that caused it.
Our working theory: a latent bug in tcl-tls's cleanup path, triggered when the AI "roast" feature's HTTPS call to Gemini got a 429 and the typo script's own allocation churn made it visible. Plausible. We shelved the script and moved on.
The second bot, built clean, crashed the same way
A few days later we built a second bot, Funball, with a typo counter written from scratch — deliberately with none of Pulsar's Gemini/HTTP/AI code, specifically to avoid the exact risk class we'd just diagnosed.
It crashed with the identical free(): invalid size signature, mid-message-handler, under fast-paced chat.
That killed the tcl-tls-plus-Gemini theory outright. Funball had no Gemini calls, no async HTTP of any kind. The one thing the two scripts still had in common was the aspell coprocess — shelling out and doing blocking pipe I/O from inside eggdrop's event loop, once per English word, per message.
Fix #1: remove the coprocess. Still crashed.
We replaced the aspell subprocess entirely: dumped its dictionary to a ~125k-word static list once at Docker build time, loaded it into memory at boot, and did a plain array lookup at runtime. No subprocess, no pipe, no per-word fork(). We validated it produced identical typo/non-typo classification to the coprocess version before shipping it.
Deployed it. Same crash. Same signature. Still specifically under fast-paced chat.
That was the point where "the aspell coprocess is the bug" stopped being a hypothesis and became something we had to actually rule out, not just plausibly explain.
Fix #2: rule out the other suspect. Still crashed.
The script also had a Hinglish-detection path — a curated whitelist plus a consonant-skeleton-gated Levenshtein fuzzy match. Pure Tcl, no subprocess, but nontrivial per-word computation. We added a kill switch and tested with it fully disabled.
Still crashed.
Both language-detection code paths were now ruled out. Whatever was corrupting the heap wasn't the aspell coprocess (removed) and wasn't the Hinglish matcher (disabled). We were out of typo-script-specific suspects.
Where we actually landed
We dropped the typo counter entirely rather than keep guessing. It's not sourced in either bot's config anymore; the files are still in the repo in case someone wants to pick this up properly later.
The honest state of this investigation is: we don't know the root cause, and we think that's worth saying plainly instead of writing a confident-sounding postmortem that isn't. Two real candidates remain, neither confirmed:
- Something in eggdrop's own message-processing path under high message rate, independent of anything typo-specific — which would make it a host- or eggdrop-version-level issue, not a script bug at all.
- A
tcl-tlsbug exposed by processing timing under load — which would make it a standing risk for any heavy per-message Tcl script on either bot, not just typo counting.
If Funball stays stable now that the typo script is fully removed, that's reasonably good evidence the bug lived in that script or its interaction with the event loop — even though we never pinned down exactly where. If it crashes again anyway, that points somewhere more fundamental, and the two bots' crash histories should stop being treated as the same investigation.
The test we should have run first, and didn't: temporarily strip every custom script down to a bare join/pubmsg logger and see if sustained fast chat alone reproduces it. That cleanly separates "any custom script under load" from "something about typo-counting logic specifically" from "unrelated to scripts entirely." We're writing it down here mostly so we don't skip it next time, and so anyone else chasing a free(): invalid size in an eggdrop script has one more data point: removing the obvious subprocess is not sufficient to rule it out.
We run CosmicIRC, a free, independent IRC network. If you want to see what came of the Dockerfile investigation this post references, or you're debugging something similar and want to compare notes, we're at irc.cosmicirc.com, #cosmic.