fix: non-blocking notify, NUL-separated process args, NULL string guards, watch TOCTOU, app-exec return

ober

1a866ad24c058b4e5f0dcc05858e658b2ac683bb

diff --git a/jerboa_qt_lifecycle.cpp b/jerboa_qt_lifecycle.cpp
index f4f9b6d..b693912 100644
--- a/jerboa_qt_lifecycle.cpp
+++ b/jerboa_qt_lifecycle.cpp
@@ -45,25 +45,17 @@ extern "C" void jerboa_qt_watch_destroyed(void *object, long callback_id) {
     }
 
     auto *qobject = static_cast<QObject *>(object);
-    bool install_connection = false;
-    {
-        std::lock_guard<std::mutex> guard(watch_mutex);
-        auto &state = watched_objects[qobject];
-        auto &ids = state.ids;
-        install_connection = ids.empty();
-        if (std::find(ids.begin(), ids.end(), callback_id) == ids.end()) {
-            ids.push_back(callback_id);
-        }
+    std::lock_guard<std::mutex> guard(watch_mutex);
+    auto &state = watched_objects[qobject];
+    auto &ids = state.ids;
+    const bool install_connection = ids.empty();
+    if (std::find(ids.begin(), ids.end(), callback_id) == ids.end()) {
+        ids.push_back(callback_id);
     }
     if (install_connection) {
-        auto connection = QObject::connect(
+        state.connection = QObject::connect(
             qobject, &QObject::destroyed,
             [qobject]() { enqueue_destroyed_ids(qobject); });
-        std::lock_guard<std::mutex> guard(watch_mutex);
-        auto found = watched_objects.find(qobject);
-        if (found != watched_objects.end()) {
-            found->second.connection = connection;
-        }
     }
 }
 
@@ -129,8 +121,10 @@ extern "C" void jerboa_qt_notify_destroying_tree(void *object) {
     } else {
         Sdeactivate_thread();
         const bool invoked = QMetaObject::invokeMethod(
-            root, collect, Qt::BlockingQueuedConnection);
-        (void)Sactivate_thread();
+            root, collect, Qt::QueuedConnection);
+        if (Sactivate_thread() != 0) {
+            return;
+        }
         if (!invoked) {
             return;
         }
diff --git a/jerboa_qt_shim.c b/jerboa_qt_shim.c
index e9513c1..b876ed0 100644
--- a/jerboa_qt_shim.c
+++ b/jerboa_qt_shim.c
@@ -381,3 +381,19 @@ void jerboa_qt_scintilla_on_modified(void* s, long callback_id) {
         qt_scintilla_on_modified(s, chez_void_callback, callback_id);
 }
 #endif
+
+/* NULL-safe string getters: the Chez FFI string return type cannot represent
+ * NULL.  These wrappers substitute an empty string so the Scheme side never
+ * receives a dangling pointer. */
+const char *jerboa_qt_label_text(void *label) {
+    const char *s = qt_label_text(label);
+    return s ? s : "";
+}
+const char *jerboa_qt_push_button_text(void *button) {
+    const char *s = qt_push_button_text(button);
+    return s ? s : "";
+}
+const char *jerboa_qt_line_edit_text(void *edit) {
+    const char *s = qt_line_edit_text(edit);
+    return s ? s : "";
+}
diff --git a/lib/jerboa-qt/ffi.sls b/lib/jerboa-qt/ffi.sls
index 3287cee..d462162 100644
--- a/lib/jerboa-qt/ffi.sls
+++ b/lib/jerboa-qt/ffi.sls
@@ -1399,7 +1399,7 @@
     void)
   (define-qt-ffi
     ffi-qt-label-text
-    "qt_label_text"
+    "jerboa_qt_label_text"
     (void*)
     string)
   (define-qt-ffi
@@ -1429,7 +1429,7 @@
     void)
   (define-qt-ffi
     ffi-qt-push-button-text
-    "qt_push_button_text"
+    "jerboa_qt_push_button_text"
     (void*)
     string)
   (define-qt-ffi
@@ -1454,7 +1454,7 @@
     void)
   (define-qt-ffi
     ffi-qt-line-edit-text
-    "qt_line_edit_text"
+    "jerboa_qt_line_edit_text"
     (void*)
     string)
   (define-qt-ffi
diff --git a/lib/jerboa-qt/qt.sls b/lib/jerboa-qt/qt.sls
index c6b2b19..3b2c241 100644
--- a/lib/jerboa-qt/qt.sls
+++ b/lib/jerboa-qt/qt.sls
@@ -790,16 +790,18 @@
        (require-qt-owner! 'qt-pump-events!)
        (ffi-qt-app-process-events app)
        (qt-dispatch-events!))
-  (def (qt-app-exec! app . args) (require-qt-owner! 'qt-app-exec!)
-       (ffi-qt-app-exec app)
-       (let ([tick (if (null? args) #f (car args))])
-         (let loop ()
-           (when (= (ffi-qt-app-is-running) 1)
-             (ffi-qt-app-process-events app)
-             (qt-dispatch-events!)
-             (when tick (tick))
-             (sleep-ms 10)
-             (loop))))
+  (def (qt-app-exec! app . args)
+       (require-qt-owner! 'qt-app-exec!)
+       (let ([tick (if (null? args) #f (car args))]
+             [rc (ffi-qt-app-exec app)])
+         (when (= rc 0)
+           (let loop ()
+             (when (= (ffi-qt-app-is-running) 1)
+               (ffi-qt-app-process-events app)
+               (qt-dispatch-events!)
+               (when tick (tick))
+               (sleep-ms 10)
+               (loop)))))
        (qt-dispatch-events!))
   (def (qt-app-quit! app)
        (require-qt-owner! 'qt-app-quit!)
@@ -2605,7 +2607,7 @@
          [(parent) (ffi-qt-process-create (or parent 0))]))
   (def (qt-process-start! proc program args)
        (let ([args-str (if (list? args)
-                           (string-join args "\n")
+                           (string-join args (string #\nul))
                            args)])
          (ffi-qt-process-start proc program args-str)))
   (def (qt-process-write! proc data)
diff --git a/src/jerboa-qt/ffi.ss b/src/jerboa-qt/ffi.ss
index 556d8e6..4ed9e7f 100644
--- a/src/jerboa-qt/ffi.ss
+++ b/src/jerboa-qt/ffi.ss
@@ -1123,7 +1123,7 @@
 
   (define-qt-ffi ffi-qt-label-set-text "qt_label_set_text" (void* string) void)
 
-  (define-qt-ffi ffi-qt-label-text "qt_label_text" (void*) string)
+  (define-qt-ffi ffi-qt-label-text "jerboa_qt_label_text" (void*) string)
 
   (define-qt-ffi ffi-qt-label-set-alignment "qt_label_set_alignment" (void* int) void)
 
@@ -1139,7 +1139,7 @@
 
   (define-qt-ffi ffi-qt-push-button-set-text "qt_push_button_set_text" (void* string) void)
 
-  (define-qt-ffi ffi-qt-push-button-text "qt_push_button_text" (void*) string)
+  (define-qt-ffi ffi-qt-push-button-text "jerboa_qt_push_button_text" (void*) string)
 
   (define-qt-ffi ffi-qt-push-button-on-clicked "jerboa_qt_push_button_on_clicked" (void* long) void)
 
@@ -1153,7 +1153,7 @@
 
   (define-qt-ffi ffi-qt-line-edit-set-text "qt_line_edit_set_text" (void* string) void)
 
-  (define-qt-ffi ffi-qt-line-edit-text "qt_line_edit_text" (void*) string)
+  (define-qt-ffi ffi-qt-line-edit-text "jerboa_qt_line_edit_text" (void*) string)
 
   (define-qt-ffi ffi-qt-line-edit-set-placeholder "qt_line_edit_set_placeholder" (void* string) void)
 
diff --git a/src/jerboa-qt/qt.ss b/src/jerboa-qt/qt.ss
index 67bc3b0..7c2f001 100644
--- a/src/jerboa-qt/qt.ss
+++ b/src/jerboa-qt/qt.ss
@@ -974,18 +974,16 @@
 
   (def (qt-app-exec! app . args)
     (require-qt-owner! 'qt-app-exec!)
-    ;; qt_application_exec is a no-op in the external shim.  On Linux the Qt
-    ;; loop runs on its native pthread; this owner-thread poll drains copied
-    ;; callbacks without ever attaching that pthread to Chez.
-    (ffi-qt-app-exec app)
-    (let ((tick (if (null? args) #f (car args))))
-      (let loop ()
-        (when (= (ffi-qt-app-is-running) 1)
-          (ffi-qt-app-process-events app)
-          (qt-dispatch-events!)
-          (when tick (tick))
-          (sleep-ms 10)
-          (loop))))
+    (let ((tick (if (null? args) #f (car args)))
+          (rc (ffi-qt-app-exec app)))
+      (when (= rc 0)
+        (let loop ()
+          (when (= (ffi-qt-app-is-running) 1)
+            (ffi-qt-app-process-events app)
+            (qt-dispatch-events!)
+            (when tick (tick))
+            (sleep-ms 10)
+            (loop)))))
     (qt-dispatch-events!))
 
   (def (qt-app-quit! app)
@@ -2745,7 +2743,7 @@
       [(parent) (ffi-qt-process-create (or parent 0))]))
 
   (def (qt-process-start! proc program args)
-    (let ([args-str (if (list? args) (string-join args "\n") args)])
+    (let ([args-str (if (list? args) (string-join args (string #\nul)) args)])
       (ffi-qt-process-start proc program args-str)))
   (def (qt-process-write! proc data) (ffi-qt-process-write proc data))
   (def (qt-process-close-write! proc) (ffi-qt-process-close-write proc))