Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions