Skip to content

Add additional check for NULL text in GtkEditable

Arjan Molenaar requested to merge amolenaar/gtk:editable-check-text into main

Although text should be a string, it is possible that (e.g via language bindings or GValue) a NULL text is entered. When this happens, the app just crashes.

This changes adds a not-NULL check for text input to ensure that whatever is provided is valid.

The program below simulates the crash. Note that in Gaphor, the crash is happening in slightly different conditions, when the GtkExpression in the xml is validated.

import gi
gi.require_version("Gtk", "4.0")

from gi.repository import Gio, GObject, Gtk


xml = """\
<?xml version='1.0' encoding='UTF-8'?>
<interface>
  <requires lib="gtk" version="4.6"/>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkEditableLabel" id="text">
        <style>
          <class name="row"/>
        </style>
        <binding name="text">
          <lookup name="parameter" type="ExampleViewNode">
            <lookup name="item">GtkListItem</lookup>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>
"""

class ExampleViewNode(GObject.Object):
    __gtype_name__ = "ExampleViewNode"

    def __init__(self):
        super().__init__()

    @GObject.Property(type=str)
    def parameter(self) -> str:
        # Technically this should always be a string
        return None

    @parameter.setter  # type: ignore[no-redef]
    def parameter(self, value):
        print("new parameter value:", value)


def build_ui():
    model = Gio.ListStore.new(ExampleViewNode)
    model.append(ExampleViewNode())

    list_view =  Gtk.ListView.new()
    selection = Gtk.SingleSelection.new(model)
    list_view.set_model(selection)

    factory = Gtk.SignalListItemFactory.new()
    factory.connect(
        "setup",
        list_item_factory_setup,
    )

    list_view.set_factory(factory)
    return list_view


def list_item_factory_setup(_factory, list_item):
    builder = Gtk.Builder()
    builder.set_current_object(list_item)
    builder.extend_with_template(
        list_item,
        type(list_item).__gtype__,
        xml,
        -1,
    )


if __name__ == "__main__":

    Gtk.init()

    def on_activate(app):
        list_view = build_ui()

        win = Gtk.Window()
        win.set_child(list_view)
        win.set_visible(True)

        app.add_window(win)

    app = Gtk.Application(application_id="org.gtk.ExpressionBindNull")
    app.connect("activate", on_activate)
    app.run()

On a crash an backtrace like this is created:

#0  __strlen_evex () at ../sysdeps/x86_64/multiarch/strlen-evex.S:88
#1  0x00007f04feb0ba21 in gtk_editable_insert_text (editable=0x55f58aec3ac0, text=0x0, length=-1, position=0x7ffd495be8c4) at ../gtk/gtkeditable.c:487
#2  0x00007f04feb0befc in gtk_editable_set_text (text=0x0, editable=0x55f58aec3ac0) at ../gtk/gtkeditable.c:600
#3  gtk_editable_set_text (editable=editable@entry=0x55f58aec3ac0, text=0x0) at ../gtk/gtkeditable.c:590
#4  0x00007f04feb0cd6f in gtk_editable_delegate_set_property
    (object=object@entry=0x55f58aea73f0, prop_id=prop_id@entry=2, value=value@entry=0x7ffd495bea40, pspec=pspec@entry=0x55f58aea6240)
    at ../gtk/gtkeditable.c:1078
#5  0x00007f04feb107a1 in gtk_editable_label_set_property (object=0x55f58aea73f0, prop_id=2, value=0x7ffd495bea40, pspec=0x55f58aea6240)
    at ../gtk/gtkeditablelabel.c:281
#6  0x00007f050037b71a in object_set_property
    (object=object@entry=0x55f58aea73f0, pspec=0x55f58aea6240, value=value@entry=0x7ffd495bea40, nqueue=nqueue@entry=0x0, user_specified=user_specified@entry=1) at ../gobject/gobject.c:1812
#7  0x00007f050037e657 in g_object_setv (values=<optimized out>, names=<optimized out>, n_properties=<optimized out>, object=0x55f58aea73f0)
    at ../gobject/gobject.c:2723
#8  g_object_setv (object=0x55f58aea73f0, n_properties=<optimized out>, names=<optimized out>, values=<optimized out>) at ../gobject/gobject.c:2694
#9  0x00007f050037e891 in g_object_set_property (object=<optimized out>, property_name=<optimized out>, value=value@entry=0x7ffd495bea40)
    at ../gobject/gobject.c:3023
#10 0x00007f04feb1c77c in gtk_expression_bind_notify (data=0x55f58aea4dc0) at ../gtk/gtkexpression.c:2101
#11 0x00007f050036b4ea in g_closure_invoke
    (closure=0x55f58aec69e0, return_value=0x0, n_param_values=2, param_values=0x7ffd495bec20, invocation_hint=0x7ffd495beba0) at ../gobject/gclosure.c:832
#12 0x00007f0500399e16 in signal_emit_unlocked_R.isra.0
    (node=node@entry=0x55f58aa46840, detail=detail@entry=867, instance=instance@entry=0x55f58aea2ab0, emission_return=emission_return@entry=0x0, instance_and_params=instance_and_params@entry=0x7ffd495bec20) at ../gobject/gsignal.c:3812
#13 0x00007f050038acbd in g_signal_emit_valist
    (instance=<optimized out>, signal_id=<optimized out>, detail=<optimized out>, var_args=var_args@entry=0x7ffd495bede0) at ../gobject/gsignal.c:3565
#14 0x00007f050038af33 in g_signal_emit (instance=<optimized out>, signal_id=<optimized out>, detail=<optimized out>) at ../gobject/gsignal.c:3622
#15 0x00007f05003766b4 in g_object_dispatch_properties_changed (object=0x55f58aea2ab0, n_pspecs=<optimized out>, pspecs=<optimized out>)

This MR makes sure the app is not crashing and a message is printed. There may be a deeper underlying reason why the GValue has a NULL-string value.

See also: https://github.com/gaphor/gaphor/issues/2433

Merge request reports