Skip to content

GNOME Shell SIGSEGV: skin-tone/gender settings handlers in EmojiSearchItem fire on a disposed St.Entry #145

Description

@PhilMeyr

Summary

GNOME Shell crashes with SIGSEGV (signal 11) when the changed::skin-tone / changed::gender settings handlers registered in EmojiSearchItem fire on a stale instance whose St.Entry has already been disposed. The handlers are connected in the EmojiSearchItem constructor but are never disconnected, so they outlive the entry they reference. When the setting later changes (e.g. clicking a skin-tone button), the orphaned handler calls this.searchEntry.get_text() on a destroyed St.Entry, which hits clutter_text_get_text: assertion 'CLUTTER_IS_TEXT (self)' failed and segfaults the whole shell.

This was introduced by #126 (skin-tone matching on search, shipped in v33) and is still present in v34 (latest).

Environment

Extension version 34 (latest; bug also in master)
GNOME Shell 50.1 (Ubuntu 26.04, Wayland)
Mutter libmutter-clutter-18.so.0
Signal 11 (SIGSEGV)

Crash traces

GJS stack trace (from the system journal):

Object St.Entry (0x...), has been already disposed — impossible to access it.
  #0  emojiSearchItem.js:112   (let searchedText = this.searchEntry.get_text();)
  #1  emojiSearchItem.js:36    (changed::skin-tone handler → this._onSearchTextChanged())
  #3  emojiOptionsBar.js:112   (this._settings.set_int("skin-tone", 0);)
clutter_text_get_text: assertion 'CLUTTER_IS_TEXT (self)' failed
GNOME Shell crashed with signal 11

JS ERROR: TypeError: can't access property "toLowerCase", searchedText is null
  _onSearchTextChanged@emojiSearchItem.js:118
  EmojiSearchItem/<@emojiSearchItem.js:36
  _buildToneButton/<@emojiOptionsBar.js:112

Native backtrace (from the apport core dump):

StacktraceTop:
 clutter_text_get_text () from /usr/lib/x86_64-linux-gnu/mutter-18/libmutter-clutter-18.so.0
 ?? () from libffi.so.8
 ffi_call () from libffi.so.8
 ?? () from libgjs.so.0

The native frame confirms the JS call searchEntry.get_text() reaches clutter_text_get_text() on a Clutter.Text that has already been freed, via the GJS → libffi → C bridge.

Root cause

In emojiSearchItem.js, the constructor connects two settings handlers but does not store the handler IDs and never disconnects them:

// emojiSearchItem.js, constructor
if (this._settings && this._settings.connect) {
  this._settings.connect('changed::skin-tone', () => {
    this._onSearchTextChanged();           // line 36
  });
  this._settings.connect('changed::gender', () => {
    this._onSearchTextChanged();
  });
}

EmojiSearchItem has no destroy() method, and two code paths drop a searchItem (and dispose its St.Entry) without disconnecting these handlers:

  1. updateNbCols() (extension.js) replaces the instance and leaks the old one:
    this.searchItem = new EmojiSearchItem(this, nbCols); // old instance + its skin-tone/gender handlers leaked
  2. disable() (extension.js) destroys super_btn (which disposes the entry) and nulls searchItem/_settings, but never disconnects the skin-tone/gender handlers:
    this.super_btn.destroy();   // disposes searchEntry
    this.searchItem = null;     // handlers on _settings still connected
    this._settings = null;
    (Note: disable() also forgets this._settings.disconnect(this.signaux[3]) — the changed::nbcols handler is leaked too.)

Because GNOME caches Gio.Settings backends per schema, a write through any live wrapper for org.gnome.shell.extensions.emoji-copy notifies all still-connected handlers — including the orphaned one bound to a disposed entry. The handler then runs _onSearchTextChanged(), whose very first line dereferences the dead entry:

_onSearchTextChanged() {
  let searchedText = this.searchEntry.get_text(); // disposed St.Entry → CLUTTER_IS_TEXT assertion → SIGSEGV
  ...
  searchedText = searchedText.toLowerCase().trim(); // get_text() returns null on disposed entry → TypeError

The TypeError: ... toLowerCase ... null is the JS-visible symptom; the fatal event is the native use-after-free in clutter_text_get_text.

Steps to reproduce

  1. Install/enable emoji-copy v33 or v34.
  2. Trigger at least one disable → enable cycle without restarting GNOME Shell — e.g. lock and unlock the screen (Super+L), suspend/resume, toggle the extension off/on, or change "number of columns" in Preferences (the updateNbCols() path). This leaves an orphaned skin-tone/gender handler bound to a now-disposed St.Entry.
  3. Open the emoji panel and click a skin-tone (or gender) button.
  4. → GNOME Shell crashes with SIGSEGV; the whole session is torn down.

In practice this happens silently after any lock/unlock or suspend/resume during a normal work session, then the next skin-tone click crashes the desktop.

Proposed fix

Track the handler IDs, add a destroy() that disconnects them, call it whenever a searchItem is dropped, and make _onSearchTextChanged() defensive.

emojiSearchItem.js — store IDs in the constructor:

this._settingsHandlerIds = [];
if (this._settings && this._settings.connect) {
  this._settingsHandlerIds.push(
    this._settings.connect('changed::skin-tone', () => this._onSearchTextChanged()),
    this._settings.connect('changed::gender',    () => this._onSearchTextChanged()),
  );
}

Add a destroy():

destroy() {
  if (this._settings && this._settingsHandlerIds) {
    for (const id of this._settingsHandlerIds)
      this._settings.disconnect(id);
  }
  this._settingsHandlerIds = [];
  this.super_item?.destroy();
}

Guard _onSearchTextChanged() (belt-and-suspenders):

_onSearchTextChanged() {
  let searchedText = this.searchEntry?.get_text();
  if (searchedText === null || searchedText === undefined || searchedText === "") {
    this._buildRecents();
    this._updateSensitivity();
    return;
  }
  searchedText = searchedText.toLowerCase().trim();
  ...
}

extension.js — destroy the old instance before replacing / disabling, and fix the leaked nbcols handler:

updateNbCols() {
  let nbCols = this._settings.get_int("nbcols");
  this.emojiCategories.forEach((c) => c.setNbCols(nbCols));
  this.searchItem?.destroy();
  this.searchItem = new EmojiSearchItem(this, nbCols);
}

disable() {
  this.searchItem.saveRecents();
  if (this._settings.get_boolean("active-keybind"))
    Main.wm.removeKeybinding("emoji-keybind");

  this._settings.disconnect(this.signaux[0]);
  this._settings.disconnect(this.signaux[1]);
  this._settings.disconnect(this.signaux[2]);
  this._settings.disconnect(this.signaux[3]); // was leaked

  if (this.timeoutSourceId) {
    GLib.Source.remove(this.timeoutSourceId);
    this.timeoutSourceId = null;
  }

  this.sqlite.destroy();
  this.searchItem.destroy(); // disconnect skin-tone/gender handlers
  this.super_btn.destroy();
  this.searchItem = null;
  this._settings = null;
  this.super_btn = null;
  this.sqlite = null;
  this.signaux = [];
}

(Minor, while in there: enable() calls searchItem._recentlyUsedInit() a second time at line 87 even though the constructor already calls it at line 46 — the duplicate rebuilds _recents and is harmless but unnecessary.)

Happy to open a PR if that helps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions