Clasp version: clasp-boehmprecise-2.7.0-892-g7eb263ba3 (boehmprecise, non-cst), macOS arm64
gctools:deallocate-unmanaged-instance — the function static-vectors:free-static-vector on Clasp — has two defects. The first is cosmetic and 100% reproducible; the second is a heap-corruption bug proven from the source.
1. A debug printf is left in the deallocator (100% reproducible)
Every call prints to stdout:
../src/gctools/gc_interface.cc:128 Calculated jump_table_index 332
from obj_deallocate_unmanaged_instance (src/gctools/gc_interface.cc:128):
size_t jump_table_index = (size_t)stamp; // - stamp_first_general;
printf("%s:%d Calculated jump_table_index %lu\n", __FILE__, __LINE__, jump_table_index);
The reproducer below emits this line once per free. Any program that frees static vectors floods stdout.
2. GC_FREE is called on an interior pointer (heap corruption)
The deallocation path passes the client/object pointer to Boehm's GC_FREE, but GC_FREE requires the base pointer that GC_MALLOC returned. Clasp objects are laid out as Header_s immediately followed by the client object, so the client pointer is base + sizeof(Header_s) — an interior pointer. The chain, all from current source:
obj_deallocate_unmanaged_instance → generated obj_deallocate_unmanaged_instance_STAMPWTAG_* (generated/clasp_gc.cc): OT* obj_gc_safe = reinterpret_cast<OT*>(client); GC<OT>::deallocate_unmanaged_instance(obj_gc_safe); — still the client pointer.
GC<OT>::deallocate_unmanaged_instance (include/clasp/gctools/gcalloc.h:346) → GCObjectAllocator<OT>::deallocate(obj) (gcalloc.h:245) → do_free(memory) with memory == client.
do_free (include/clasp/gctools/gcalloc_boehm.h:143): inline void do_free(void* ptr) { GC_FREE(ptr); }.
That GC_FREE(client) most likely is wrong: the allocation side (do_uncollectable_allocation, same file) returns a Header_s* base, and the lisp object handed out is HeaderPtrToGeneralPtr(base) = base + sizeof(Header_s) (GeneralPtrToHeaderPtr confirms the offset, memoryManagement.h:837). The same file already does it correctly in deallocate_dynamic_object (gcalloc_boehm.h ~121–132): it keeps base, hands out HeaderPtrToGeneralPtr(base), and frees with GC_FREE(base) — not the client.
Per the Boehm contract, GC_FREE on a non-base pointer is undefined and corrupts the allocator's free lists. The visible consequence is an intermittent SIGSEGV/SIGILL inside GC_malloc_kind on a later allocation (heap-layout/ASLR dependent, so it does not reliably crash in a tiny script — but it reliably corrupts; we hit the crash repeatably under a multithreaded test suite that frees static vectors).
Reproducer
(require :asdf)
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
(ql:quickload :static-vectors)
(dotimes (i 100)
(let ((v (static-vectors:make-static-vector 2048 :element-type '(unsigned-byte 8))))
(setf (aref v 0) 1)
(static-vectors:free-static-vector v)))
Run with clasp --non-interactive --load repro.lisp. Observed: 100 lines of gc_interface.cc:128 Calculated jump_table_index 332, one per free.
Suggested fix
Free the base, not the client — e.g. do_free(GeneralPtrToHeaderPtr(memory)) in GCObjectAllocator<OT>::deallocate (or have obj_deallocate_unmanaged_instance pass the header it already computes), mirroring deallocate_dynamic_object. And remove/guard the gc_interface.cc:128 printf behind a debug flag.
Clasp version:
clasp-boehmprecise-2.7.0-892-g7eb263ba3(boehmprecise, non-cst), macOS arm64gctools:deallocate-unmanaged-instance— the functionstatic-vectors:free-static-vectoron Clasp — has two defects. The first is cosmetic and 100% reproducible; the second is a heap-corruption bug proven from the source.1. A debug
printfis left in the deallocator (100% reproducible)Every call prints to stdout:
from
obj_deallocate_unmanaged_instance(src/gctools/gc_interface.cc:128):The reproducer below emits this line once per free. Any program that frees static vectors floods stdout.
2.
GC_FREEis called on an interior pointer (heap corruption)The deallocation path passes the client/object pointer to Boehm's
GC_FREE, butGC_FREErequires the base pointer thatGC_MALLOCreturned. Clasp objects are laid out asHeader_simmediately followed by the client object, so the client pointer isbase + sizeof(Header_s)— an interior pointer. The chain, all from current source:obj_deallocate_unmanaged_instance→ generatedobj_deallocate_unmanaged_instance_STAMPWTAG_*(generated/clasp_gc.cc):OT* obj_gc_safe = reinterpret_cast<OT*>(client); GC<OT>::deallocate_unmanaged_instance(obj_gc_safe);— still the client pointer.GC<OT>::deallocate_unmanaged_instance(include/clasp/gctools/gcalloc.h:346) →GCObjectAllocator<OT>::deallocate(obj)(gcalloc.h:245) →do_free(memory)withmemory == client.do_free(include/clasp/gctools/gcalloc_boehm.h:143):inline void do_free(void* ptr) { GC_FREE(ptr); }.That
GC_FREE(client)most likely is wrong: the allocation side (do_uncollectable_allocation, same file) returns aHeader_s*base, and the lisp object handed out isHeaderPtrToGeneralPtr(base)=base + sizeof(Header_s)(GeneralPtrToHeaderPtrconfirms the offset,memoryManagement.h:837). The same file already does it correctly indeallocate_dynamic_object(gcalloc_boehm.h~121–132): it keepsbase, hands outHeaderPtrToGeneralPtr(base), and frees withGC_FREE(base)— not the client.Per the Boehm contract,
GC_FREEon a non-base pointer is undefined and corrupts the allocator's free lists. The visible consequence is an intermittentSIGSEGV/SIGILLinsideGC_malloc_kindon a later allocation (heap-layout/ASLR dependent, so it does not reliably crash in a tiny script — but it reliably corrupts; we hit the crash repeatably under a multithreaded test suite that frees static vectors).Reproducer
Run with
clasp --non-interactive --load repro.lisp. Observed: 100 lines ofgc_interface.cc:128 Calculated jump_table_index 332, one per free.Suggested fix
Free the base, not the client — e.g.
do_free(GeneralPtrToHeaderPtr(memory))inGCObjectAllocator<OT>::deallocate(or haveobj_deallocate_unmanaged_instancepass theheaderit already computes), mirroringdeallocate_dynamic_object. And remove/guard thegc_interface.cc:128printfbehind a debug flag.