Skip to content

gctools:deallocate-unmanaged-instance frees an interior pointer (corrupts Boehm heap) + leaks a debug printf #1793

@dg1sbg

Description

@dg1sbg

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions