[PATCH/bug] macOS arm64: build fixes + global-GC-during-image-load crash

Quinton Quartel <[email protected]> Fri, 19 Jun 2026 12:16:56 -0400
Newsgroups gmane.comp.lang.smalltalk.gnu.general
Message-ID <CABghsJ287yf3hBehEyd8X2vxSRzP1gHRvPpvC3h9-rJ8npULmQ@mail.gmail.com>
Hello,

While getting GNU Smalltalk 3.2.92 (git 768d3ef4) building and running on
Apple Silicon, I found three small build/portability issues and root-caused
a crash that fails ~36 of the "make check" tests. This continues the arm64
work from bf3fd4b5 ("mem: macos on arm64 does not allow write (and read) on
executable pages").

Environment: macOS 26.5.1, arm64, 16384-byte pages, Apple clang 21.0.0,
libsigsegv/libffi/gmp/gnutls from Homebrew.

================================================================
PART 1 - Build patches (ready to apply)
================================================================

--- 1a. lib-src/getopt.c : <string.h> not included on non-glibc ---

The non-__GNU_LIBRARY__ branch never includes <string.h>, so strcmp/strlen
are implicitly declared -- a hard error under modern clang.

@@ -204,12 +204,14 @@ static char *posixly_correct;
/* Avoid depending on library functions or files
whose names are inconsistent. */

+/* Modern non-GNU C libraries (macOS, *BSD, ...) ship a clean <string.h>;
+ include it so strcmp/strncmp/strlen are properly prototyped rather than
+ implicitly declared (a hard error under recent compilers). */
+#include <string.h>
+
#ifndef getenv
extern char *getenv ();
#endif
-#ifndef strncmp
-extern int strncmp ();
-#endif

--- 1b. packages/xml/expat/expat.c : stale XML_SkippedEntityHandler
signature ---

Modern expat's XML_SkippedEntityHandler typedef gained a third
"int is_parameter_entity" parameter.

@@ -478,7 +478,8 @@ gst_EndNamespaceDeclHandler (void *userData,
static void
gst_SkippedEntityHandler (void *userData,
- const XML_Char * entityName)
+ const XML_Char * entityName,
+ int is_parameter_entity)
{

--- 1c. libgst/sysdep/posix/mem.c : _gst_osmem_alloc still requests
PROT_EXEC ---

bf3fd4b5 removed PROT_EXEC from anon_mmap_commit, but the sibling
_gst_osmem_alloc was missed. On Apple Silicon, mmap(PROT_READ|PROT_WRITE|
PROT_EXEC, MAP_ANON|MAP_PRIVATE) fails with EACCES (verified with a
standalone
C program), so this returns NULL for large (>256 KB) blocks. Object-heap
pages
are never executed, so the same fix applies:

@@ -156,7 +156,7 @@ _gst_osmem_alloc (size_t size)
PTR addr;
addr = mmap (NULL, size,
- PROT_READ | PROT_WRITE | PROT_EXEC,
+ PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);

(Note: 1c is a real latent bug but is NOT the cause of the crash in Part 2
--
that path only fires for allocations larger than MMAP_THRESHOLD = 256 KB,
which the failing tests never make.)

Build recipe used:
AUTOPOINT=true autoreconf -vi
./configure ac_cv_prog_cc_c23=no CFLAGS="-g -O2"
make
(ac_cv_prog_cc_c23=no is needed because C23 removes the K&R function
definitions still used in lib-src/{regex,poll,md5}.c.)

================================================================
PART 2 - Crash: global GC during image load dereferences NULL
================================================================

Symptom: "make check" reports ~36 failures (most of the *ANSITest suite,
plus
https://www.google.com/url?q=http://cobjects.st&source=gmail&ust=1781971893182000&sa=E).
They are not assertion failures; the VM aborts inside
_gst_global_gc / _gst_fixup_object_pointers.

Reproduction:
./gst -I tests/
https://www.google.com/url?q=http://gst.im&source=gmail&ust=1781971893182000&sa=E
-f tests/AnsiRun.st FloatANSITest
Crashes ~75% of runs (non-deterministic / ASLR-dependent). The small default
https://www.google.com/url?q=http://gst.im&source=gmail&ust=1781971893182000&sa=E
(4 MB) loads fine; the larger tests/
https://www.google.com/url?q=http://gst.im&source=gmail&ust=1781971893182000&sa=E
(7 MB) triggers it.

Root cause -- symbolized backtrace (from a macOS DiagnosticReport):

empty_context_stack+1244 <-- READ of address 0x0 (NULL)
_gst_fixup_object_pointers+72
_gst_global_gc+316
oldspace_nomemory+240
_gst_mem_alloc+1040
load_normal_oops+296 <-- still LOADING THE IMAGE
load_snapshot+492
_gst_load_from_file
_gst_initialize
main

The crash happens during image load, not during test execution:

1. load_snapshot calls _gst_init_mem(..., header.oldSpaceSize, ...)
(save.c:494), sizing oldspace to the value saved in the image.
2. load_normal_oops allocates each loaded object into oldspace. On arm64 the
objects exceed the provisioned oldspace and _gst_mem_alloc calls
oldspace_nomemory.
3. oldspace_nomemory (oop.c:910) sees !_gst_gc_running and runs a full
global
GC in the middle of loading the image.
4. That GC calls _gst_fixup_object_pointers -> empty_context_stack
(interp.c:701), which walks the interpreter context chain. The context
state has not been reconstructed yet during load, so it dereferences NULL.

Confirming experiment: provisioning more oldspace at load
(header.oldSpaceSize
* 8 at save.c:494) eliminates the crash entirely -- 100% -> 0/10 runs,
FloatANSITest passes 81/81. This confirms the mechanism: oldspace exhaustion
during load -> unsafe mid-load global GC.

Why arm64-specific / non-deterministic:
- The same logical object set over-fills oldspace on arm64 where it does not
elsewhere -- most likely 16 KB-page rounding of heap blocks in alloc.c
inflating per-allocation cost (this exact over-fill cause is unconfirmed).
- The SIGSEGV write barrier (oldspace_sigsegv_handler) sometimes recovers
the
faulting access, which is why the normal build is only ~75% fatal. Forcing
NO_SIGSEGV_HANDLING makes it a deterministic 100% NULL-read SEGV.

Suggested fix directions (I have not implemented these -- they touch GC/load
invariants and felt like maintainer territory):
- Grow, don't GC, during image load. A global GC mid-load is both pointless
(all loaded objects are live) and unsafe (contexts are not reconstructed).
Routing oldspace_nomemory to grow while a load is in progress is the
smallest principled fix.
- Or fix the arm64 oldspace sizing so loading the image never exhausts it.

Debugging methodology, for anyone reproducing:
- lldb is NOT useful: it disables ASLR, which changes the heap layout enough
to hide the crash.
- AddressSanitizer confirms "SEGV READ at 0x0" but does not unwind
DEADLYSIGNAL on macOS.
- What worked: build with NO_SIGSEGV_HANDLING (deterministic 100% crash),
run
the repro, and read the symbolized report macOS writes to
~/Library/Logs/DiagnosticReports/gst-*.ips.

Separately:
https://www.google.com/url?q=http://cobjects.st&source=gmail&ust=1781971893182000&sa=E
and the Digest test fail with a deterministic
SystemExceptions.CInterfaceError (FFI / C-module loading) -- a different
issue
from the GC crash above.

Happy to test patches or provide more detail. I'd love to be added as a
contributor and can submit the fixes I have for these bugs.

Thanks,
Quinton Quartel