[feature/igc3] weak hash table lookup can return a finalized (PVEC_FREE) object
| Newsgroups | gmane.emacs.devel |
|---|---|
| Message-ID | <[email protected]> |
Hi. This is about an immediate crash upon startup with igc3 and doom emacs.
What follows is a LLM-produced report and attached a suggested
fix that has been working stable for a few weeks now.
I can reproduce reliably and retrieve more debug information if needed.
Given the report and fix size I hope this is helpful.
----------------
Branch feature/igc3, commit 07b35c82b7d ("Move 'read_commit_limit' to
sysdep.c"), x86_64-pc-linux-gnu, --with-mps.
A lookup in a weak hash table can return an object that the collector
has already finalized and splatted into a PVEC_FREE. Lisp then sees a
"function" that is not a function, and printing the resulting error
aborts Emacs.
Reproducer, "emacs -Q --batch -l repro.el", no init file involved:
;; -*- lexical-binding: t -*-
(defvar h (make-hash-table :test #'equal :weakness 'value))
(defun add (i) (puthash (list 'k i) (lambda () i) h))
(dotimes (i 100) (add i)) ; values reachable only from H
(igc--collect) ; values die
(igc--process-messages) ; finalization splats them
(princ (format "%s\n" (gethash (list 'k 0) h)))
This aborts in print_vectorlike_unreadable. Without the final line,
(let ((v (gethash (list 'k 0) h)))
(princ (format "non-nil: %S functionp: %S\n" (and v t) (functionp v)))
(condition-case e (funcall v)
(error (princ (format "funcall signalled: %S\n" (car e))))))
prints
non-nil: t functionp: nil
funcall signalled: invalid-function
The sequence is:
1. weak_hash_put calls Figc__add_extra_dependency on the weakly held
side of the entry (src/fns.c:5900-5903). That goes through
igc_external_header (src/igc.c:5763), which gives the object an
external header and registers it with mps_finalize.
2. The value becomes unreachable. MPS posts a finalization message.
The weak reference in the table is *not* splatted in that trace: the
final ring keeps the object reachable at a rank stronger than weak,
so the entry survives the collection intact.
3. Emacs processes the message. finalize (src/igc.c) restores the
header from the external header and calls finalize_vector, which ends
in splat_pvec (src/igc.c:4438) - the object's pseudovector tag is
overwritten with PVEC_FREE, in place, while the table entry still
points at it.
4. The entry is only removed one cycle later, when
fix_weak_hash_table_weak_part (src/igc.c:3011) sees the weak
reference go nil and calls weak_hash_splat_from_table.
So there is a window of at least a full GC cycle in which a splatted
object is still present in the table and still matches a lookup. This
is not a race; in the reproducer it is deterministic.
DOHASH_WEAK (src/lisp.h:3066) already skips entries whose key or value
is PVEC_FREE, for exactly this reason. The lookup path has no such
check: weak_hash_lookup_with_hash (src/fns.c:5863) matches on the key
alone and Fgethash (src/fns.c:6666) returns WEAK_HASH_VALUE
unconditionally.
How I ran into it: Emacs crashed about a second into starting a Doom
Emacs configuration, every time. The victim was a combined method in
cl--generic-combined-method-memoization, which is
(make-hash-table :test #'equal :weakness 'value)
Two of its values are splatted on every startup (the cl-generic-
generalizers methods for EIEIO defclass and for cl-deftype derived
types). gethash returned one of them, funcall signalled
(invalid-function #<splatted closure>), and the error handler formatted
the error with %s, which reached print_vectorlike_unreadable and
emacs_abort. In the core dump the table's kv.values vector still holds
two PVEC_FREE objects, next to a live key
(#s(cl--generic cl-generic-generalizers ...) . METHODS).
Proposed fix, attached as igc-weak-hash-fix.diff: skip dead entries in
the
lookup, the way DOHASH_WEAK already does.
With this, both reproducers above are clean (gethash reports a miss) and
the Doom configuration starts normally. I instrumented the two skips
before removing the counters: exactly two lookups per startup matched a
key whose value had been splatted, reproducibly, on every start.
Two further points, which I have not tried to fix:
- finalize (src/igc.c) only dispatches to finalize_vector; everything
else falls through "default: break". So the PVEC_FREE marker exists
only for pseudovectors. A weak table whose weakly held side is a
cons, string or symbol goes through the same window with no marker at
all, and neither DOHASH_WEAK nor the check above can detect it - the
lookup will hand back an object that should have been evicted.
- It may be worth asking whether the entry can be removed at the same
time as the splat, rather than a cycle later, which would close the
window instead of papering over it.
The attachment also contains a second, optional hunk: making
print_vectorlike_unreadable print "#<dead object>" for PVEC_FREE under
HAVE_MPS instead of calling emacs_abort, so that this class of bug does
not turn a Lisp-level error into a crash while the error is being
printed.
-----------------
In GNU Emacs 32.0.50 (build 2, x86_64-pc-linux-gnu, GTK+ Version
3.24.52, cairo version 1.18.4) of 2026-08-28 built on amn
Repository revision: 07b35c82b7dfc9d0c044dc54dcc65675f49de87e
Repository branch: feature/igc3
System Description: Arch Linux
Configured using:
'configure NATIVE_FULL_AOT=1 'CFLAGS=-O2 -g -fno-omit-frame-pointer
-march=native' --prefix=/home/user/.local --with-native-compilation=aot
--with-pgtk=yes --with-tree-sitter --with-small-ja-dic --with-mps
--with-webkit'
Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
LCMS2 LIBOTF LIBSYSTEMD LIBXML2 MODULES MPS NATIVE_COMP NOTIFY INOTIFY
PDUMPER PGTK PNG RSVG SECCOMP SOUND SQLITE3 THREADS TIFF
TOOLKIT_SCROLL_BARS TREE_SITTER WEBP XIM GTK3 ZLIB
Important settings:
value of $LC_ALL: en_US.UTF-8
value of $LC_TIME: pt_BR.UTF-8
value of $LANG: en_US.UTF-8
locale-coding-system: utf-8-unix
-----------------
igc-weak-hash-fix.diff
(text/x-patch, 2.2 KB)
diff --git a/src/fns.c b/src/fns.c
index 580d10907ab..77030bd8cab 100644
--- a/src/fns.c
+++ b/src/fns.c
@@ -5867,12 +5867,31 @@ weak_hash_lookup_with_hash (struct Lisp_Weak_Hash_Table *h,
ptrdiff_t start_of_bucket = weak_hash_index_index (h, hash);
for (ptrdiff_t i = WEAK_HASH_INDEX (h, start_of_bucket);
0 <= i; i = WEAK_HASH_NEXT (h, i))
- if (EQ (key, WEAK_HASH_KEY (h, i))
- || (h->strong->h.test->cmpfn
- && hash == WEAK_HASH_HASH (h, i)
- && !NILP (h->strong->h.test->cmpfn (key, WEAK_HASH_KEY (h, i),
- NULL /* XXX */))))
+ {
+ Lisp_Object k = WEAK_HASH_KEY (h, i);
+
+ /* An entry whose key or value has already been finalized and
+ splatted into a PVEC_FREE is dead, it merely hasn't been
+ removed from the table yet. Never let such an object escape
+ into Lisp; DOHASH_WEAK skips those entries for the same
+ reason. Comparing against a splatted key is not safe either. */
+ if (PSEUDOVECTORP (k, PVEC_FREE))
+ continue;
+
+ if (!(EQ (key, k)
+ || (h->strong->h.test->cmpfn
+ && hash == WEAK_HASH_HASH (h, i)
+ && !NILP (h->strong->h.test->cmpfn (key, k,
+ NULL /* XXX */)))))
+ continue;
+
+ /* The key matched, but the value is already dead. Report a miss
+ rather than handing the dead object out. */
+ if (PSEUDOVECTORP (WEAK_HASH_VALUE (h, i), PVEC_FREE))
+ continue;
+
return i;
+ }
return -1;
}
diff --git a/src/print.c b/src/print.c
index 62359787960..f2e831224f2 100644
--- a/src/print.c
+++ b/src/print.c
@@ -2186,8 +2186,20 @@ print_vectorlike_unreadable (Lisp_Object obj, bool escapeflag, char *buf,
#endif
case PVEC_BIGNUM:
case PVEC_BOOL_VECTOR:
- /* Impossible cases. */
+ break;
+
+#ifdef HAVE_MPS
+ /* A finalized object that was splatted by the GC. It should never
+ become visible to Lisp, but if it does, aborting while printing
+ the error that reports it makes the bug much harder to find. */
+ case PVEC_FREE:
+ print_c_string ("#<dead object>", printcharfun);
+ return;
+#else
case PVEC_FREE:
+#endif
+
+ /* Impossible cases. */
case PVEC_OTHER:
case PVEC_MODULE_GLOBAL_REFERENCE:
break;