Repository navigation
Modules call core variadic functions through unprototyped pointers (segfaults on Apple arm64) #1940
Description
Summary
On macOS/arm64, eggdrop segfaults shortly after startup, in varying places. The
common factor is always a module calling one of the core's variadic
functions through the global[] function table, which is typed as an
unprototyped pointer. On AArch64 under Apple's ABI, variadic and non-variadic
arguments use different conventions, so the caller passes arguments in registers
while the callee reads them off the stack. The result is a garbage pointer
handed to strlen.
This is benign on x86-64, where the two conventions coincide which is likely
why it has gone unnoticed.
Environment
- Eggdrop 1.10.2 (release tarball) and master (
1.10.2+alpha, commit d93dbd7) are both affected - macOS 26.0 (Darwin 25.6.0), Apple silicon (arm64)
- Tcl 9.0.4, OpenSSL 3.6.4 (Homebrew)
- Apple clang,
-O2(default) and-O0 ./configure --with-tclinc=... --with-tcllib=... --with-sslinc=... --with-ssllib=...
The defect
src/eggdrop.h:
typedef intptr_t (*Function) (); /* unprototyped */src/mod/module.h:
#define egg_snprintf (global[252]) /* variadic in core */
#define simple_sprintf ((int (*)())global[23]) /* variadic in core */Both egg_snprintf (→ snprintf) and simple_sprintf are variadic, but a
module calls them through a pointer with no prototype. Apple's ARM64 ABI
documentation states that variadic arguments are passed differently from
named ones and that code passing arguments to variadic functions must be
updated for arm64; calling a variadic function through a non-variadic pointer
is undefined behaviour and breaks there in practice.
Symptoms
Four distinct crash sites observed, all matching that pattern of a module
calling a core string-formatting function:
| site | call |
|---|---|
channels.c:551 write_channels |
egg_snprintf(s, sizeof s, "%s~new", chanfile) |
server.mod connect_server |
egg_snprintf(s, sizeof s, "%s [%s]", IRC_SERVERTRY, botserver) |
irc.mod setaccount (from gotjoin) |
snprintf |
irc.mod check_tcl_need (from check_expired_chanstuff) |
simple_sprintf |
AddressSanitizer, with SIGSEGV handler disabled so the report is not swallowed:
==43243==ERROR: AddressSanitizer: SEGV on unknown address 0x000041b58ab3
#0 __sanitizer::internal_strlen(char const*)
#1 vsnprintf
#2 snprintf
#3 write_channels channels.c:551
#4 chanprog chanprog.c:440
#5 main main.c:1075
SUMMARY: AddressSanitizer: SEGV channels.c:551 in write_channels
The setaccount crash is the clearest evidence of the mechanism as the faulting
address is 0x6861676967000000, which is the ASCII bytes of the bot's own nick:
a string being dereferenced as a pointer, i.e. arguments read from the wrong place.
Reproduction
Minimal config below. It also crashes with stock eggdrop.conf.
set username "testbot"
set nick "testbot"
set altnick "testbot_"
set realname "test"
set owner "me"
set botnet-nick "testbot"
set userfile "repro.user"
set chanfile "repro.chan"
set pidfile "repro.pid"
addlang "english"
loadmodule blowfish
loadmodule channels
loadmodule server
loadmodule ctcp
loadmodule irc
server add irc.libera.chat +6697
channel add #somechannel./eggdrop -m -n ./repro.conf → SEGMENT VIOLATION within seconds.
Corroboration from within the same header
The two variadic core functions that modules call without trouble are
declared with full prototypes, including ...:
typedef void (*putlogfunc)(int, char*, const char*, ...) ATTRIBUTE_FORMAT(printf,3,4);
typedef void (*dprintffunc)(int, const char *, ...) ATTRIBUTE_FORMAT(printf,2,3);putlog and dprintf are called constantly from modules and never misbehave.
The difference between them and egg_snprintf/simple_sprintf is exactly the
prototype.
Tested fix
--- a/src/mod/module.h
+++ b/src/mod/module.h
-#define simple_sprintf ((int (*)())global[23])
+#define simple_sprintf ((int (*)(char *, const char *, ...))global[23])
-#define egg_snprintf (global[252])
+#define egg_snprintf ((int (*)(char *, size_t, const char *, ...))global[252])With those two lines changed and nothing else, the same bot on the same machine
connects, joins a channel and stays up. Before the patch it died within 30 seconds on every attempt.
Function being intptr_t (*)() means the compiler cannot warn about any of
this, so an audit of the rest of the table for other variadic members is
probably worthwhile rather than only fixing these two.
Activity
michaelortmann commentedon Sep 22, 2026
This is related to a more general issue with prototypes/eggdrop module API, we noticed when clang introduced new warning:
passing arguments to a function without a prototype is deprecated in all versions of C and is not supported in C23 [-Wdeprecated-non-prototype]This bug report elevates this issue from level warning to crash.
I suspect we didnt receive such crash report before, because Apple has a different ABI than other popular arm64 systems like Raspberry Pi.
Sadly, I cannot reproduce/fix yet, because i dont have such an Apple arm64 system.
michaelortmann commentedon Sep 22, 2026
This is also related to #1028, where i want to clean up compat cruft, replacing
egg_snprintf()withsnprintf().mateuszkwiatkowski commentedon Sep 22, 2026
@michaelortmann I'm happy to test any patches on my hardware.
michaelortmann commentedon Oct 6, 2026
Waiting for #1942