From 0ab9561d2b439a1975041586b59417471c5a3aea Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 11:24:45 +0700 Subject: [PATCH 01/22] security: wipe passphrases, bind hidden stash to SSID, re-register agent Wipe passphrase memory in the auth and hidden-network dialogs (explicit_bzero on owned copies plus overwriting the elm_entry buffer before destruction) so secrets don't linger on the heap. Bind the hidden-network passphrase stash to its SSID with a 30s timeout, so a typo'd or out-of-range hidden connect can't leak its passphrase to an unrelated network whose RequestPassphrase happens to land first. Re-RegisterAgent on iwd NameOwnerChanged so PSK connects survive systemctl restart iwd instead of silently hanging. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_main.c | 1 + src/e_mod_popup.c | 111 +++++++++++++++++++++++++++++------------- src/e_mod_popup.h | 7 +-- src/iwd/iwd_agent.c | 16 +++--- src/iwd/iwd_agent.h | 4 ++ src/iwd/iwd_manager.c | 9 +++- src/ui/wifi_auth.c | 31 ++++++++++-- src/ui/wifi_hidden.c | 33 +++++++++++-- 8 files changed, 161 insertions(+), 51 deletions(-) diff --git a/src/e_mod_main.c b/src/e_mod_main.c index 160316a..bcd8f60 100644 --- a/src/e_mod_main.c +++ b/src/e_mod_main.c @@ -42,6 +42,7 @@ e_modapi_shutdown(E_Module *m EINA_UNUSED) if (!e_iwd) return 1; e_iwd_gadget_shutdown(); + e_iwd_popup_shutdown(); if (e_iwd->manager) iwd_manager_free(e_iwd->manager); e_iwd_config_save(); if (e_iwd->conn) eldbus_connection_unref(e_iwd->conn); diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index c987cf2..94b777a 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -30,8 +30,39 @@ static Popup *_popup = NULL; static Iwd_Agent_Request *_pending_req = NULL; /* Tracked so iwd's Cancel(reason) can tear down the dialog. */ static Evas_Object *_pending_dialog = NULL; -/* One-shot passphrase pre-armed by the hidden-network dialog. */ -static char *_hidden_pending_pass = NULL; +/* One-shot passphrase pre-armed by the hidden-network dialog. Bound to the + * SSID it was entered for, with a timeout that wipes it if iwd never comes + * asking — so a stashed passphrase can never leak to an unrelated network. */ +static char *_hidden_pending_pass = NULL; +static char *_hidden_pending_ssid = NULL; +static Ecore_Timer *_hidden_pending_timer = NULL; +#define HIDDEN_PASS_TIMEOUT 30.0 /* seconds */ + +static void +_hidden_pending_clear(void) +{ + if (_hidden_pending_pass) + { + explicit_bzero(_hidden_pending_pass, strlen(_hidden_pending_pass)); + free(_hidden_pending_pass); + _hidden_pending_pass = NULL; + } + free(_hidden_pending_ssid); + _hidden_pending_ssid = NULL; + if (_hidden_pending_timer) + { + ecore_timer_del(_hidden_pending_timer); + _hidden_pending_timer = NULL; + } +} + +static Eina_Bool +_hidden_pending_timeout(void *data EINA_UNUSED) +{ + _hidden_pending_timer = NULL; + _hidden_pending_clear(); + return ECORE_CALLBACK_CANCEL; +} /* ----- helpers --------------------------------------------------------- */ @@ -249,10 +280,19 @@ _on_hidden_done(void *data EINA_UNUSED, const char *ssid, const char *pass, Eina if (!ok || !ssid || !*ssid) return; Iwd_Device *dev = _active_device(); if (!dev) return; - /* Pre-arm the agent reply so the next RequestPassphrase from iwd is - * answered automatically. If the network turns out to be open, the - * stashed passphrase is simply never consumed. */ - if (pass && *pass) _hidden_pending_pass = strdup(pass); + /* Pre-arm the agent reply so the next RequestPassphrase from iwd for + * *this SSID* is answered automatically. The stash is bound to the SSID + * and self-clears after HIDDEN_PASS_TIMEOUT seconds — so a typo'd or + * out-of-range SSID cannot cause the passphrase to leak to a later, + * unrelated network whose RequestPassphrase happens to land first. */ + _hidden_pending_clear(); + if (pass && *pass) + { + _hidden_pending_pass = strdup(pass); + _hidden_pending_ssid = strdup(ssid); + _hidden_pending_timer = ecore_timer_add(HIDDEN_PASS_TIMEOUT, + _hidden_pending_timeout, NULL); + } iwd_device_connect_hidden(dev, ssid); } @@ -267,7 +307,12 @@ static void _on_auth_done(void *data EINA_UNUSED, const char *pass, Eina_Bool ok) { _pending_dialog = NULL; - if (!_pending_req) return; + if (!_pending_req) + { + /* Request was already canceled by iwd; nothing to do. The caller + * (wifi_auth) wipes its own copy of `pass` after we return. */ + return; + } if (ok) iwd_agent_reply (_pending_req, pass ? pass : ""); else iwd_agent_cancel(_pending_req); _pending_req = NULL; @@ -303,15 +348,27 @@ e_iwd_popup_install_passphrase_handler(void) static void _on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const char *netpath) { - /* If the user just kicked off a hidden-network connect with a passphrase, - * answer this request automatically without prompting. */ - if (_hidden_pending_pass) + /* Resolve netpath -> network so we can both match the hidden stash + * against the requested SSID *and* show a friendly label in the dialog. */ + Iwd_Network *n = NULL; + if (e_iwd && e_iwd->manager) + { + const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); + if (h) n = eina_hash_find(h, netpath); + } + const char *req_ssid = (n && n->ssid) ? n->ssid : NULL; + + /* Use the hidden-network stash *only* if iwd is asking for the same + * SSID we entered it for. Anything else: drop the stash on the floor + * and prompt normally. */ + if (_hidden_pending_pass && _hidden_pending_ssid && + req_ssid && !strcmp(req_ssid, _hidden_pending_ssid)) { iwd_agent_reply(req, _hidden_pending_pass); - free(_hidden_pending_pass); - _hidden_pending_pass = NULL; + _hidden_pending_clear(); return; } + if (_pending_req) { iwd_agent_cancel(req); @@ -319,27 +376,8 @@ _on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const cha } _pending_req = req; - /* Look up the network for a friendly SSID, if we have it. */ - const char *ssid = "network"; - if (e_iwd && e_iwd->manager) - { - const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); - if (h) - { - Iwd_Network *n = eina_hash_find(h, netpath); - if (n && n->ssid) ssid = n->ssid; - } - } - const char *sec = NULL; - if (e_iwd && e_iwd->manager) - { - const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); - if (h) - { - Iwd_Network *n = eina_hash_find(h, netpath); - if (n) sec = _sec_label(n->security); - } - } + const char *ssid = req_ssid ? req_ssid : "network"; + const char *sec = n ? _sec_label(n->security) : NULL; _pending_dialog = wifi_auth_prompt(_popup ? _popup->box : e_comp->elm, ssid, sec, _on_auth_done, NULL); } @@ -357,6 +395,13 @@ _destroy(void) _popup = NULL; } +void +e_iwd_popup_shutdown(void) +{ + _destroy(); + _hidden_pending_clear(); +} + void e_iwd_popup_close(void) { _destroy(); } diff --git a/src/e_mod_popup.h b/src/e_mod_popup.h index 33717a9..a053212 100644 --- a/src/e_mod_popup.h +++ b/src/e_mod_popup.h @@ -4,8 +4,9 @@ #include void e_iwd_popup_install_passphrase_handler(void); -void e_iwd_popup_toggle (E_Gadcon_Client *gcc); -void e_iwd_popup_close (void); -void e_iwd_popup_refresh(void); +void e_iwd_popup_toggle (E_Gadcon_Client *gcc); +void e_iwd_popup_close (void); +void e_iwd_popup_refresh (void); +void e_iwd_popup_shutdown(void); #endif diff --git a/src/iwd/iwd_agent.c b/src/iwd/iwd_agent.c index 6947f90..74c67f1 100644 --- a/src/iwd/iwd_agent.c +++ b/src/iwd/iwd_agent.c @@ -143,6 +143,14 @@ _on_register(void *data EINA_UNUSED, const Eldbus_Message *msg, fprintf(stderr, "e_iwd: agent register failed: %s: %s\n", en, em); } +void +iwd_agent_register(Iwd_Agent *a) +{ + if (!a || !a->am_proxy) return; + eldbus_proxy_call(a->am_proxy, "RegisterAgent", _on_register, NULL, -1, + "o", IWD_AGENT_PATH); +} + Iwd_Agent * iwd_agent_new(Eldbus_Connection *conn, Iwd_Agent_Passphrase_Cb cb, void *data) { @@ -158,12 +166,8 @@ iwd_agent_new(Eldbus_Connection *conn, Iwd_Agent_Passphrase_Cb cb, void *data) a->am_obj = eldbus_object_get(conn, IWD_BUS_NAME, "/net/connman/iwd"); if (a->am_obj) - { - a->am_proxy = eldbus_proxy_get(a->am_obj, IWD_IFACE_AGENT_MANAGER); - if (a->am_proxy) - eldbus_proxy_call(a->am_proxy, "RegisterAgent", _on_register, NULL, -1, - "o", IWD_AGENT_PATH); - } + a->am_proxy = eldbus_proxy_get(a->am_obj, IWD_IFACE_AGENT_MANAGER); + iwd_agent_register(a); return a; } diff --git a/src/iwd/iwd_agent.h b/src/iwd/iwd_agent.h index 9fcc168..49e6b65 100644 --- a/src/iwd/iwd_agent.h +++ b/src/iwd/iwd_agent.h @@ -21,6 +21,10 @@ Iwd_Agent *iwd_agent_new (Eldbus_Connection *conn, Iwd_Agent_Passphrase_Cb cb, void *data); void iwd_agent_set_cancel_cb(Iwd_Agent *a, Iwd_Agent_Cancel_Cb cb, void *data); +/* Re-issue RegisterAgent. Call after iwd reappears on the bus + * (NameOwnerChanged) — without this, every PSK connect silently hangs + * because no agent is registered against the new iwd instance. */ +void iwd_agent_register(Iwd_Agent *a); void iwd_agent_free(Iwd_Agent *a); void iwd_agent_reply (Iwd_Agent_Request *req, const char *passphrase); diff --git a/src/iwd/iwd_manager.c b/src/iwd/iwd_manager.c index 5cd78a2..49b9576 100644 --- a/src/iwd/iwd_manager.c +++ b/src/iwd/iwd_manager.c @@ -222,7 +222,14 @@ _on_iface_removed(void *data, const char *path, const char *iface) } static void -_on_name_appeared(void *data EINA_UNUSED) { /* GetManagedObjects will populate */ } +_on_name_appeared(void *data) +{ + /* GetManagedObjects will repopulate adapters/devices/networks; we just + * need to re-register our agent against the new iwd instance. Without + * this, PSK connects silently hang after `systemctl restart iwd`. */ + Iwd_Manager *m = data; + if (m && m->agent) iwd_agent_register(m->agent); +} static void _on_name_vanished(void *data) diff --git a/src/ui/wifi_auth.c b/src/ui/wifi_auth.c index 8fdb1e5..24d21d3 100644 --- a/src/ui/wifi_auth.c +++ b/src/ui/wifi_auth.c @@ -1,5 +1,6 @@ #include "wifi_auth.h" #include +#include typedef struct _Auth_Ctx { @@ -12,11 +13,27 @@ typedef struct _Auth_Ctx } Auth_Ctx; static void -_finish(Auth_Ctx *c, Eina_Bool ok, const char *pass) +_finish(Auth_Ctx *c, Eina_Bool ok) { if (c->fired) return; c->fired = EINA_TRUE; + + /* Copy the passphrase into a buffer we own so we can wipe it + * after the callback returns. The elm_entry's internal buffer + * is then overwritten before the window (and entry) are destroyed. */ + char *pass = NULL; + if (ok && c->entry) + { + const char *raw = elm_entry_entry_get(c->entry); + if (raw) pass = strdup(raw); + } if (c->cb) c->cb(c->data, pass, ok); + if (pass) + { + explicit_bzero(pass, strlen(pass)); + free(pass); + } + if (c->entry) elm_entry_entry_set(c->entry, ""); if (c->win) evas_object_del(c->win); free(c); } @@ -24,21 +41,25 @@ _finish(Auth_Ctx *c, Eina_Bool ok, const char *pass) static void _on_ok(void *data, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) { - Auth_Ctx *c = data; - _finish(c, EINA_TRUE, elm_entry_entry_get(c->entry)); + _finish(data, EINA_TRUE); } static void _on_cancel(void *data, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) { - _finish(data, EINA_FALSE, NULL); + _finish(data, EINA_FALSE); } static void _on_del(void *data, Evas *e EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) { /* Window closed without cancel/ok — treat as cancel. */ - _finish(data, EINA_FALSE, NULL); + Auth_Ctx *c = data; + /* The window (and entry) are being destroyed; null entry to skip the + * post-cb entry_set in _finish. */ + c->win = NULL; + c->entry = NULL; + _finish(c, EINA_FALSE); } Evas_Object * diff --git a/src/ui/wifi_hidden.c b/src/ui/wifi_hidden.c index 1e936f8..3070944 100644 --- a/src/ui/wifi_hidden.c +++ b/src/ui/wifi_hidden.c @@ -1,5 +1,6 @@ #include "wifi_hidden.h" #include +#include typedef struct _Hidden_Ctx { @@ -17,9 +18,31 @@ _finish(Hidden_Ctx *c, Eina_Bool ok) { if (c->fired) return; c->fired = EINA_TRUE; - const char *ssid = ok ? elm_entry_entry_get(c->e_ssid) : NULL; - const char *pass = ok ? elm_entry_entry_get(c->e_pass) : NULL; + + /* Copy SSID + passphrase into buffers we own; wipe the passphrase + * (and overwrite the entry) before the window is destroyed. */ + char *ssid = NULL, *pass = NULL; + if (ok) + { + if (c->e_ssid) + { + const char *r = elm_entry_entry_get(c->e_ssid); + if (r) ssid = strdup(r); + } + if (c->e_pass) + { + const char *r = elm_entry_entry_get(c->e_pass); + if (r) pass = strdup(r); + } + } if (c->cb) c->cb(c->data, ssid, pass, ok); + if (pass) + { + explicit_bzero(pass, strlen(pass)); + free(pass); + } + free(ssid); + if (c->e_pass) elm_entry_entry_set(c->e_pass, ""); if (c->win) evas_object_del(c->win); free(c); } @@ -42,7 +65,11 @@ _on_cancel(void *data, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) static void _on_del(void *data, Evas *e EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) { - _finish(data, EINA_FALSE); + Hidden_Ctx *c = data; + c->win = NULL; + c->e_ssid = NULL; + c->e_pass = NULL; + _finish(c, EINA_FALSE); } static Evas_Object * From 47d70ab78d0c136df4fcc4ddda1174ae75a7d624 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 11:25:56 +0700 Subject: [PATCH 02/22] gadget: stringshare gadcon id instead of static buffer A static char[128] returned from _gc_id_new is overwritten on every call, so multiple gadget instances would alias the same id once gadcon compares or stores it. eina_stringshare_add gives each instance its own stable id. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_gadget.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/e_mod_gadget.c b/src/e_mod_gadget.c index 45d278f..e3036d4 100644 --- a/src/e_mod_gadget.c +++ b/src/e_mod_gadget.c @@ -259,10 +259,10 @@ _gc_icon(const E_Gadcon_Client_Class *cc EINA_UNUSED, Evas *evas) static const char * _gc_id_new(const E_Gadcon_Client_Class *cc) { - static char buf[128]; + char buf[128]; snprintf(buf, sizeof(buf), "%s.%d", cc->name, eina_list_count(_instances) + 1); - return buf; + return eina_stringshare_add(buf); } static const E_Gadcon_Client_Class _gadcon_class = From 3fc41eef8f864fee8b8dffc5b2cb3b51d3643af3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 11:26:25 +0700 Subject: [PATCH 03/22] agent: UnregisterAgent on free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tell iwd to drop our registration during shutdown instead of relying on NameOwnerChanged GC. Avoids spurious agent calls landing while the service interface is being torn down. Fire-and-forget — the reply, if any, is irrelevant. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/iwd/iwd_agent.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/iwd/iwd_agent.c b/src/iwd/iwd_agent.c index 74c67f1..f78c558 100644 --- a/src/iwd/iwd_agent.c +++ b/src/iwd/iwd_agent.c @@ -183,6 +183,13 @@ void iwd_agent_free(Iwd_Agent *a) { if (!a) return; + /* Politely deregister so iwd doesn't keep dispatching to a dead service + * during shutdown. Fire-and-forget: the connection may already be torn + * down by the time the call would land, and there's nothing to do with + * the reply anyway. */ + if (a->am_proxy) + eldbus_proxy_call(a->am_proxy, "UnregisterAgent", NULL, NULL, -1, + "o", IWD_AGENT_PATH); if (a->svc) eldbus_service_interface_unregister(a->svc); if (a->am_proxy) eldbus_proxy_unref(a->am_proxy); if (a->am_obj) eldbus_object_unref(a->am_obj); From 0418e8bab9b435eff3ca42f5e450f8f243f46f85 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 11:29:59 +0700 Subject: [PATCH 04/22] manager: surface D-Bus call errors to the user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connect / Forget / Set(Powered) / Scan / Disconnect / RegisterAgent / ConnectHidden previously discarded reply errors with NULL callbacks, so "Connecting…" could hang forever after a refused call (rfkill, busy adapter, another agent already registered, bad credentials on a known network). The user had no way to see the failure. Add iwd_manager_{report,last,clear}_error and wire reply callbacks in adapter / device / network / agent. The popup status line now appends the latest error to the state label, and user actions (rescan, toggle, connect, disconnect) clear it. Scan errors that mean "already in flight" are filtered out — they're the normal race when two scan triggers fire close together. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 22 +++++++++++++++--- src/iwd/iwd_adapter.c | 12 +++++++++- src/iwd/iwd_agent.c | 15 +++++++++--- src/iwd/iwd_device.c | 54 +++++++++++++++++++++++++++++++++++-------- src/iwd/iwd_manager.c | 31 +++++++++++++++++++++++++ src/iwd/iwd_manager.h | 9 ++++++++ src/iwd/iwd_network.c | 46 +++++++++++++++++++++++++++++------- 7 files changed, 165 insertions(+), 24 deletions(-) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 94b777a..c55a9f3 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -148,6 +148,7 @@ _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) { Iwd_Network *n = data; if (!n) return; + if (e_iwd && e_iwd->manager) iwd_manager_clear_error(e_iwd->manager); iwd_network_connect(n); } @@ -234,7 +235,17 @@ _refresh(Popup *p) if (!p || !e_iwd || !e_iwd->manager) return; Iwd_State s = iwd_manager_state(e_iwd->manager); if (p->status_lbl) - elm_object_text_set(p->status_lbl, _state_label(s)); + { + const char *err = iwd_manager_last_error(e_iwd->manager); + if (err) + { + char buf[320]; + snprintf(buf, sizeof(buf), "%s — %s", _state_label(s), err); + elm_object_text_set(p->status_lbl, buf); + } + else + elm_object_text_set(p->status_lbl, _state_label(s)); + } if (p->btn_toggle) elm_object_text_set(p->btn_toggle, s == IWD_STATE_OFF ? "Enable" : "Disable"); if (p->btn_scan) @@ -260,18 +271,23 @@ _on_manager_change(void *data, Iwd_Manager *m EINA_UNUSED) static void _on_rescan (void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { - if (e_iwd && e_iwd->manager) iwd_manager_scan_request(e_iwd->manager); + if (!e_iwd || !e_iwd->manager) return; + iwd_manager_clear_error(e_iwd->manager); + iwd_manager_scan_request(e_iwd->manager); } static void _on_toggle(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { if (!e_iwd || !e_iwd->manager) return; + iwd_manager_clear_error(e_iwd->manager); Eina_Bool off = (iwd_manager_state(e_iwd->manager) == IWD_STATE_OFF); iwd_manager_set_powered(e_iwd->manager, off); } static void _on_disconnect(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED) { Iwd_Device *dev = _active_device(); - if (dev) iwd_device_disconnect(dev); + if (!dev) return; + if (e_iwd && e_iwd->manager) iwd_manager_clear_error(e_iwd->manager); + iwd_device_disconnect(dev); } static void diff --git a/src/iwd/iwd_adapter.c b/src/iwd/iwd_adapter.c index 4747dad..8998d39 100644 --- a/src/iwd/iwd_adapter.c +++ b/src/iwd/iwd_adapter.c @@ -61,6 +61,16 @@ iwd_adapter_free(Iwd_Adapter *a) free(a); } +static void +_on_set_powered_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) +{ + Iwd_Adapter *a = data; + const char *en, *em; + if (eldbus_message_error_get(msg, &en, &em) && a->manager) + iwd_manager_report_error(a->manager, + "Set Adapter.Powered failed: %s", em ? em : en); +} + void iwd_adapter_set_powered(Iwd_Adapter *a, Eina_Bool on) { @@ -83,7 +93,7 @@ iwd_adapter_set_powered(Iwd_Adapter *a, Eina_Bool on) eldbus_message_iter_basic_append(variant, 'b', v); eldbus_message_iter_container_close(iter, variant); - eldbus_proxy_send(props, msg, NULL, NULL, -1); + eldbus_proxy_send(props, msg, _on_set_powered_reply, a, -1); /* Keep the props proxy alive on the adapter so the call isn't canceled. */ if (a->_props_proxy_keepalive) eldbus_proxy_unref(a->_props_proxy_keepalive); a->_props_proxy_keepalive = props; diff --git a/src/iwd/iwd_agent.c b/src/iwd/iwd_agent.c index f78c558..e7cc974 100644 --- a/src/iwd/iwd_agent.c +++ b/src/iwd/iwd_agent.c @@ -1,5 +1,7 @@ #include "iwd_agent.h" #include "iwd_dbus.h" +#include "iwd_manager.h" +#include #include #include @@ -135,11 +137,18 @@ iwd_agent_cancel(Iwd_Agent_Request *req) /* ----- Registration with iwd ------------------------------------------ */ static void -_on_register(void *data EINA_UNUSED, const Eldbus_Message *msg, +_on_register(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { + /* `data` is the manager — same pointer the trampoline carries. */ + Iwd_Manager *m = data; const char *en, *em; - if (eldbus_message_error_get(msg, &en, &em)) + if (!eldbus_message_error_get(msg, &en, &em)) return; + if (m) + iwd_manager_report_error(m, + "Wi-Fi agent registration refused (another agent running?): %s", + em ? em : en); + else fprintf(stderr, "e_iwd: agent register failed: %s: %s\n", en, em); } @@ -147,7 +156,7 @@ void iwd_agent_register(Iwd_Agent *a) { if (!a || !a->am_proxy) return; - eldbus_proxy_call(a->am_proxy, "RegisterAgent", _on_register, NULL, -1, + eldbus_proxy_call(a->am_proxy, "RegisterAgent", _on_register, a->data, -1, "o", IWD_AGENT_PATH); } diff --git a/src/iwd/iwd_device.c b/src/iwd/iwd_device.c index 1c05c52..b8942c9 100644 --- a/src/iwd/iwd_device.c +++ b/src/iwd/iwd_device.c @@ -3,7 +3,6 @@ #include "iwd_props.h" #include "iwd_manager.h" #include "iwd_network.h" -#include #include #include @@ -186,35 +185,72 @@ _refresh_signals(Iwd_Device *d) _on_ordered_networks, d, -1, ""); } +static void +_on_scan_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) +{ + Iwd_Device *d = data; + const char *en, *em; + if (!eldbus_message_error_get(msg, &en, &em)) return; + /* "AlreadyExists" / "InProgress" is the normal race when two scan + * triggers fire close together — don't spam the user with that. */ + if (en && (strstr(en, "InProgress") || strstr(en, "Busy") || + strstr(en, "AlreadyExists"))) + return; + if (d->manager) + iwd_manager_report_error(d->manager, "Scan failed: %s", em ? em : en); +} + void iwd_device_scan(Iwd_Device *d) { if (!d || !d->station_proxy) return; - eldbus_proxy_call(d->station_proxy, "Scan", NULL, NULL, -1, ""); + eldbus_proxy_call(d->station_proxy, "Scan", _on_scan_reply, d, -1, ""); +} + +static void +_on_disconnect_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) +{ + Iwd_Device *d = data; + const char *en, *em; + if (eldbus_message_error_get(msg, &en, &em) && d->manager) + iwd_manager_report_error(d->manager, + "Disconnect failed: %s", em ? em : en); } void iwd_device_disconnect(Iwd_Device *d) { if (!d || !d->station_proxy) return; - eldbus_proxy_call(d->station_proxy, "Disconnect", NULL, NULL, -1, ""); + eldbus_proxy_call(d->station_proxy, "Disconnect", _on_disconnect_reply, d, -1, ""); } +typedef struct +{ + Iwd_Device *d; + char *ssid; +} _Connect_Hidden_Ctx; + static void _on_connect_hidden_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { + _Connect_Hidden_Ctx *ctx = data; const char *en, *em; - char *ssid = data; - if (eldbus_message_error_get(msg, &en, &em)) - fprintf(stderr, "e_iwd: ConnectHiddenNetwork('%s') failed: %s: %s\n", - ssid ? ssid : "?", en, em); - free(ssid); + if (eldbus_message_error_get(msg, &en, &em) && ctx->d->manager) + iwd_manager_report_error(ctx->d->manager, + "Connect to hidden '%s' failed: %s", + ctx->ssid ? ctx->ssid : "?", em ? em : en); + free(ctx->ssid); + free(ctx); } void iwd_device_connect_hidden(Iwd_Device *d, const char *ssid) { if (!d || !d->station_proxy || !ssid || !*ssid) return; + _Connect_Hidden_Ctx *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return; + ctx->d = d; + ctx->ssid = strdup(ssid); eldbus_proxy_call(d->station_proxy, "ConnectHiddenNetwork", - _on_connect_hidden_reply, strdup(ssid), -1, "s", ssid); + _on_connect_hidden_reply, ctx, -1, "s", ssid); } diff --git a/src/iwd/iwd_manager.c b/src/iwd/iwd_manager.c index 49b9576..941e907 100644 --- a/src/iwd/iwd_manager.c +++ b/src/iwd/iwd_manager.c @@ -4,6 +4,8 @@ #include "iwd_device.h" #include "iwd_network.h" #include +#include +#include #include #include @@ -23,6 +25,7 @@ struct _Iwd_Manager Eina_List *listeners; /* Listener * */ Iwd_State state; Ecore_Job *notify_job; + char *last_error; Iwd_Agent_Passphrase_Cb pass_cb; void *pass_data; @@ -102,6 +105,33 @@ iwd_manager_notify(Iwd_Manager *m) m->notify_job = ecore_job_add(_notify_job_cb, m); } +const char * +iwd_manager_last_error(const Iwd_Manager *m) { return m ? m->last_error : NULL; } + +void +iwd_manager_clear_error(Iwd_Manager *m) +{ + if (!m || !m->last_error) return; + free(m->last_error); + m->last_error = NULL; +} + +void +iwd_manager_report_error(Iwd_Manager *m, const char *fmt, ...) +{ + if (!m || !fmt) return; + char buf[256]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + /* stderr keeps the dev-visible trail; the stashed copy drives the UI. */ + fprintf(stderr, "e_iwd: %s\n", buf); + free(m->last_error); + m->last_error = strdup(buf); + iwd_manager_notify(m); +} + /* ----- state aggregation ---------------------------------------------- */ static void @@ -281,6 +311,7 @@ iwd_manager_free(Iwd_Manager *m) eina_hash_free(m->networks); Listener *li; EINA_LIST_FREE(m->listeners, li) free(li); + free(m->last_error); free(m); } diff --git a/src/iwd/iwd_manager.h b/src/iwd/iwd_manager.h index 900fad4..f23d3b5 100644 --- a/src/iwd/iwd_manager.h +++ b/src/iwd/iwd_manager.h @@ -36,6 +36,15 @@ void iwd_manager_listener_del (Iwd_Manager *m, Iwd_Manager_Cb cb, void *data); /* Internal: invoked by sub-objects when their state changes. */ void iwd_manager_notify (Iwd_Manager *m); +/* Latest user-facing error string, or NULL. Owned by the manager. + * Cleared on next successful state change or by iwd_manager_clear_error. */ +const char *iwd_manager_last_error (const Iwd_Manager *m); +void iwd_manager_clear_error(Iwd_Manager *m); + +/* Stash a one-shot error message and notify listeners. Used by D-Bus reply + * callbacks when iwd refuses a Connect/Forget/Set(Powered)/etc. */ +void iwd_manager_report_error(Iwd_Manager *m, const char *fmt, ...); + /* The UI installs its passphrase prompt here. The handler must * eventually call iwd_agent_reply()/iwd_agent_cancel() with the request. */ void iwd_manager_set_passphrase_handler(Iwd_Manager *m, diff --git a/src/iwd/iwd_network.c b/src/iwd/iwd_network.c index 18a84d5..29da8cd 100644 --- a/src/iwd/iwd_network.c +++ b/src/iwd/iwd_network.c @@ -2,7 +2,6 @@ #include "iwd_dbus.h" #include "iwd_props.h" #include "iwd_manager.h" -#include #include #include @@ -80,15 +79,36 @@ iwd_network_free(Iwd_Network *n) free(n); } +typedef struct +{ + Iwd_Network *n; + char *ssid; +} _Net_Reply_Ctx; + static void _on_connect_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { + _Net_Reply_Ctx *ctx = data; const char *en, *em; - const char *ssid = data; - if (eldbus_message_error_get(msg, &en, &em)) - fprintf(stderr, "e_iwd: connect to '%s' failed: %s: %s\n", - ssid ? ssid : "?", en, em); - free(data); + if (eldbus_message_error_get(msg, &en, &em) && ctx->n->manager) + iwd_manager_report_error(ctx->n->manager, + "Connect to '%s' failed: %s", + ctx->ssid ? ctx->ssid : "?", em ? em : en); + free(ctx->ssid); + free(ctx); +} + +static void +_on_forget_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) +{ + _Net_Reply_Ctx *ctx = data; + const char *en, *em; + if (eldbus_message_error_get(msg, &en, &em) && ctx->n->manager) + iwd_manager_report_error(ctx->n->manager, + "Forget '%s' failed: %s", + ctx->ssid ? ctx->ssid : "?", em ? em : en); + free(ctx->ssid); + free(ctx); } int @@ -104,6 +124,16 @@ iwd_network_signal_tier(const Iwd_Network *n) return 1; } +static _Net_Reply_Ctx * +_reply_ctx_new(Iwd_Network *n) +{ + _Net_Reply_Ctx *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return NULL; + ctx->n = n; + ctx->ssid = n->ssid ? strdup(n->ssid) : NULL; + return ctx; +} + void iwd_network_connect(Iwd_Network *n) { @@ -111,7 +141,7 @@ iwd_network_connect(Iwd_Network *n) /* Network.Connect() takes no args; iwd will dial the registered Agent * for a passphrase if needed. */ eldbus_proxy_call(n->proxy, "Connect", _on_connect_reply, - n->ssid ? strdup(n->ssid) : NULL, -1, ""); + _reply_ctx_new(n), -1, ""); } void @@ -124,7 +154,7 @@ iwd_network_forget(Iwd_Network *n) Eldbus_Proxy *kp = eldbus_proxy_get(kobj, IWD_IFACE_KNOWN_NETWORK); if (kp) { - eldbus_proxy_call(kp, "Forget", NULL, NULL, -1, ""); + eldbus_proxy_call(kp, "Forget", _on_forget_reply, _reply_ctx_new(n), -1, ""); eldbus_proxy_unref(kp); } eldbus_object_unref(kobj); From 853e6ae4546607ee64c0675c6de0564f04147c57 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 11:30:33 +0700 Subject: [PATCH 05/22] popup: confirmation dialog before Forget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forget destroys the saved passphrase irreversibly. A stray click on the ✕ next to a known network would wipe credentials with no recovery and (until the previous commit) no error feedback either. Add an elm_popup confirmation that names the SSID before invoking Forget. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index c55a9f3..3bd28bc 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -152,12 +152,53 @@ _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) iwd_network_connect(n); } +static void _forget_confirm_yes(void *data, Evas_Object *obj, void *ev EINA_UNUSED) +{ + /* The popup that owns the button is on `obj`'s parent chain — close it. */ + Iwd_Network *n = data; + if (n) iwd_network_forget(n); + Evas_Object *pp = evas_object_data_get(obj, "_eiwd_confirm_popup"); + if (pp) evas_object_del(pp); +} + +static void _forget_confirm_no(void *data EINA_UNUSED, Evas_Object *obj, void *ev EINA_UNUSED) +{ + Evas_Object *pp = evas_object_data_get(obj, "_eiwd_confirm_popup"); + if (pp) evas_object_del(pp); +} + static void _on_net_forget(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) { Iwd_Network *n = data; if (!n) return; - iwd_network_forget(n); + + /* Forget destroys the saved passphrase irreversibly — confirm first. + * A stray click on the ✕ next to a known network would otherwise wipe + * credentials with no recovery. */ + Evas_Object *parent = _popup ? _popup->box : e_comp->elm; + Evas_Object *pp = elm_popup_add(parent); + char msg[256]; + snprintf(msg, sizeof(msg), + "Forget saved network %s?
" + "The passphrase will be permanently deleted.", + n->ssid ? n->ssid : "(hidden)"); + elm_object_part_text_set(pp, "title,text", "Forget network"); + elm_object_text_set(pp, msg); + + Evas_Object *yes = elm_button_add(pp); + elm_object_text_set(yes, "Forget"); + evas_object_data_set(yes, "_eiwd_confirm_popup", pp); + evas_object_smart_callback_add(yes, "clicked", _forget_confirm_yes, n); + elm_object_part_content_set(pp, "button1", yes); + + Evas_Object *no = elm_button_add(pp); + elm_object_text_set(no, "Cancel"); + evas_object_data_set(no, "_eiwd_confirm_popup", pp); + evas_object_smart_callback_add(no, "clicked", _forget_confirm_no, NULL); + elm_object_part_content_set(pp, "button2", no); + + evas_object_show(pp); } static void From 4df3b04690dde366cf7f8e2f7377b0bb2a5f7cf6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:46:49 +0700 Subject: [PATCH 06/22] gadget: replace _theme_path static buffer with caller-provided one Static buffers in identity-like helpers are footguns: they're only safe when consumed immediately and break when callers ever stash the pointer. Take a caller-provided buffer instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_gadget.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/e_mod_gadget.c b/src/e_mod_gadget.c index e3036d4..bc7a59a 100644 --- a/src/e_mod_gadget.c +++ b/src/e_mod_gadget.c @@ -6,6 +6,7 @@ #include "iwd/iwd_device.h" #include "iwd/iwd_network.h" #include +#include /* ----- per-instance gadget data --------------------------------------- */ @@ -173,14 +174,12 @@ _on_mouse_down(void *data, Evas *e EINA_UNUSED, Evas_Object *obj EINA_UNUSED, vo /* ----- helpers --------------------------------------------------------- */ -static char * -_theme_path(void) +static Eina_Bool +_theme_path(char *buf, size_t len) { - static char buf[4096]; - if (!e_iwd || !e_iwd->module) return NULL; - snprintf(buf, sizeof(buf), "%s/e-module-iwd.edj", - e_module_dir_get(e_iwd->module)); - return buf; + if (!e_iwd || !e_iwd->module) return EINA_FALSE; + snprintf(buf, len, "%s/e-module-iwd.edj", e_module_dir_get(e_iwd->module)); + return EINA_TRUE; } /* ----- gadcon class ---------------------------------------------------- */ @@ -189,7 +188,8 @@ static E_Gadcon_Client * _gc_init(E_Gadcon *gc, const char *name, const char *id, const char *style) { Instance *inst = E_NEW(Instance, 1); - const char *path = _theme_path(); + char path[PATH_MAX]; + if (!_theme_path(path, sizeof(path))) path[0] = '\0'; /* themed edje is the gadcon o_base — its intrinsic min comes from the * theme group, just like the backlight module. */ @@ -250,9 +250,10 @@ _gc_label(const E_Gadcon_Client_Class *cc EINA_UNUSED) { return "iwd"; } static Evas_Object * _gc_icon(const E_Gadcon_Client_Class *cc EINA_UNUSED, Evas *evas) { - const char *path = _theme_path(); + char path[PATH_MAX]; Evas_Object *o = edje_object_add(evas); - if (path) edje_object_file_set(o, path, "icon"); + if (_theme_path(path, sizeof(path))) + edje_object_file_set(o, path, "icon"); return o; } From 92d4fbc5ff4a4bb5f05fecd88873ad3732faee35 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:48:03 +0700 Subject: [PATCH 07/22] popup: UTF-8-aware SSID truncation %.*s cuts at byte index, splitting multi-byte sequences and producing broken glyphs followed by the ellipsis. Walk codepoints instead and truncate at a codepoint boundary. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 3bd28bc..5417b47 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -231,13 +231,23 @@ _rebuild_list(Popup *p) evas_object_size_hint_align_set(row, EVAS_HINT_FILL, 0); Evas_Object *btn = elm_button_add(row); - /* Truncate long SSIDs so the row never forces horizontal scrolling. */ + /* Truncate long SSIDs so the row never forces horizontal scrolling. + * Count codepoints, not bytes — SSIDs may contain UTF-8 and naive + * byte truncation would split a multi-byte sequence. */ const char *raw_ssid = n->ssid ? n->ssid : "(hidden)"; - char ssid_buf[32]; - const int max_ssid = 22; - if ((int)strlen(raw_ssid) > max_ssid) + char ssid_buf[64]; + const int max_chars = 21; + int iindex = 0, prev = 0, chars = 0; + while (chars < max_chars + && eina_unicode_utf8_next_get(raw_ssid, &iindex)) + { prev = iindex; chars++; } + if (eina_unicode_utf8_next_get(raw_ssid, &iindex)) { - snprintf(ssid_buf, sizeof(ssid_buf), "%.*s…", max_ssid - 1, raw_ssid); + /* more remains — truncate at `prev` and append U+2026 */ + int copy = prev < (int)sizeof(ssid_buf) - 4 + ? prev : (int)sizeof(ssid_buf) - 4; + memcpy(ssid_buf, raw_ssid, copy); + memcpy(ssid_buf + copy, "\xe2\x80\xa6", 4); /* "…" + NUL */ raw_ssid = ssid_buf; } char label[256]; From b03d10b1648599e1f273f6c09fd10dcf2c315e45 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:48:29 +0700 Subject: [PATCH 08/22] config: use E_FREE/eina_stringshare_replace on stale config free() on memory returned by e_config_domain_load mixes allocators on the stringshare member. Use eina_stringshare_replace to drop the stringshared field and E_FREE for the struct. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_config.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/e_mod_config.c b/src/e_mod_config.c index 50bfdd6..255fc0e 100644 --- a/src/e_mod_config.c +++ b/src/e_mod_config.c @@ -30,9 +30,8 @@ e_iwd_config_load(void) /* Missing or out-of-date — start fresh with defaults. */ if (e_iwd_config) { - if (e_iwd_config->preferred_adapter) - eina_stringshare_del(e_iwd_config->preferred_adapter); - free(e_iwd_config); + eina_stringshare_replace(&e_iwd_config->preferred_adapter, NULL); + E_FREE(e_iwd_config); } e_iwd_config = E_NEW(E_Iwd_Config, 1); e_iwd_config->version = CONFIG_VERSION; From ad3d752b126333a36dd13d3689ccf7948c33c7cb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:50:01 +0700 Subject: [PATCH 09/22] iwd: extract shared state/security label helpers Both popup.c and gadget.c carried near-identical _state_label/_sec_label helpers, with the gadget version using bare ints instead of the Iwd_Security enum. Move to iwd/iwd_labels.{c,h} and use the enum consistently. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_gadget.c | 29 +++-------------------------- src/e_mod_popup.c | 36 +++++------------------------------- src/iwd/iwd_labels.c | 28 ++++++++++++++++++++++++++++ src/iwd/iwd_labels.h | 12 ++++++++++++ src/meson.build | 1 + 5 files changed, 49 insertions(+), 57 deletions(-) create mode 100644 src/iwd/iwd_labels.c create mode 100644 src/iwd/iwd_labels.h diff --git a/src/e_mod_gadget.c b/src/e_mod_gadget.c index bc7a59a..1256e2f 100644 --- a/src/e_mod_gadget.c +++ b/src/e_mod_gadget.c @@ -5,6 +5,7 @@ #include "iwd/iwd_manager.h" #include "iwd/iwd_device.h" #include "iwd/iwd_network.h" +#include "iwd/iwd_labels.h" #include #include @@ -75,30 +76,6 @@ _icon_name_for_state(Iwd_State s) return "network-wireless"; } -static const char * -_state_label(Iwd_State s) -{ - switch (s) - { - case IWD_STATE_OFF: return "Wi-Fi disabled"; - case IWD_STATE_IDLE: return "Disconnected"; - case IWD_STATE_SCANNING: return "Scanning"; - case IWD_STATE_CONNECTING: return "Connecting"; - case IWD_STATE_CONNECTED: return "Connected"; - case IWD_STATE_ERROR: return "Error"; - } - return ""; -} - -static const char * -_sec_label(int s) -{ - /* Iwd_Security values, kept in sync with iwd_network.h. */ - switch (s) { case 0: return "open"; case 1: return "WPA"; - case 2: return "802.1X"; case 3: return "WEP"; } - return "?"; -} - static void _build_tooltip(Instance *inst, Iwd_State s) { @@ -109,13 +86,13 @@ _build_tooltip(Instance *inst, Iwd_State s) if (n) snprintf(buf, sizeof(buf), "Wi-Fi: %s — %s — signal %d/4", n->ssid ? n->ssid : "?", - _sec_label(n->security), + iwd_security_label(n->security), iwd_network_signal_tier(n)); else snprintf(buf, sizeof(buf), "Wi-Fi: connected"); } else - snprintf(buf, sizeof(buf), "Wi-Fi: %s", _state_label(s)); + snprintf(buf, sizeof(buf), "Wi-Fi: %s", iwd_state_label(s)); elm_object_tooltip_text_set(inst->o_base, buf); } diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 5417b47..d1cd9b2 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -4,6 +4,7 @@ #include "iwd/iwd_device.h" #include "iwd/iwd_network.h" #include "iwd/iwd_agent.h" +#include "iwd/iwd_labels.h" #include "ui/wifi_auth.h" #include "ui/wifi_hidden.h" #include @@ -66,33 +67,6 @@ _hidden_pending_timeout(void *data EINA_UNUSED) /* ----- helpers --------------------------------------------------------- */ -static const char * -_state_label(Iwd_State s) -{ - switch (s) { - case IWD_STATE_OFF: return "Wi-Fi disabled"; - case IWD_STATE_IDLE: return "Disconnected"; - case IWD_STATE_SCANNING: return "Scanning…"; - case IWD_STATE_CONNECTING: return "Connecting…"; - case IWD_STATE_CONNECTED: return "Connected"; - case IWD_STATE_ERROR: return "Error"; - } - return ""; -} - -static const char * -_sec_label(Iwd_Security s) -{ - switch (s) { - case IWD_SEC_OPEN: return "open"; - case IWD_SEC_PSK: return "WPA"; - case IWD_SEC_8021X: return "802.1X"; - case IWD_SEC_WEP: return "WEP"; - case IWD_SEC_UNKNOWN: return "?"; - } - return ""; -} - static int _net_cmp(const void *a, const void *b) { @@ -255,7 +229,7 @@ _rebuild_list(Popup *p) _signal_bars(iwd_network_signal_tier(n)), n->known_path ? "★ " : " ", raw_ssid, - _sec_label(n->security), + iwd_security_label(n->security), n->connected ? " ✔" : ""); elm_object_text_set(btn, label); evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0); @@ -291,11 +265,11 @@ _refresh(Popup *p) if (err) { char buf[320]; - snprintf(buf, sizeof(buf), "%s — %s", _state_label(s), err); + snprintf(buf, sizeof(buf), "%s — %s", iwd_state_label(s), err); elm_object_text_set(p->status_lbl, buf); } else - elm_object_text_set(p->status_lbl, _state_label(s)); + elm_object_text_set(p->status_lbl, iwd_state_label(s)); } if (p->btn_toggle) elm_object_text_set(p->btn_toggle, s == IWD_STATE_OFF ? "Enable" : "Disable"); @@ -444,7 +418,7 @@ _on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const cha _pending_req = req; const char *ssid = req_ssid ? req_ssid : "network"; - const char *sec = n ? _sec_label(n->security) : NULL; + const char *sec = n ? iwd_security_label(n->security) : NULL; _pending_dialog = wifi_auth_prompt(_popup ? _popup->box : e_comp->elm, ssid, sec, _on_auth_done, NULL); } diff --git a/src/iwd/iwd_labels.c b/src/iwd/iwd_labels.c new file mode 100644 index 0000000..c3cfcf7 --- /dev/null +++ b/src/iwd/iwd_labels.c @@ -0,0 +1,28 @@ +#include "iwd_labels.h" + +const char * +iwd_state_label(Iwd_State s) +{ + switch (s) { + case IWD_STATE_OFF: return "Wi-Fi disabled"; + case IWD_STATE_IDLE: return "Disconnected"; + case IWD_STATE_SCANNING: return "Scanning…"; + case IWD_STATE_CONNECTING: return "Connecting…"; + case IWD_STATE_CONNECTED: return "Connected"; + case IWD_STATE_ERROR: return "Error"; + } + return ""; +} + +const char * +iwd_security_label(Iwd_Security s) +{ + switch (s) { + case IWD_SEC_OPEN: return "open"; + case IWD_SEC_PSK: return "WPA"; + case IWD_SEC_8021X: return "802.1X"; + case IWD_SEC_WEP: return "WEP"; + case IWD_SEC_UNKNOWN: return "?"; + } + return ""; +} diff --git a/src/iwd/iwd_labels.h b/src/iwd/iwd_labels.h new file mode 100644 index 0000000..9a4dee4 --- /dev/null +++ b/src/iwd/iwd_labels.h @@ -0,0 +1,12 @@ +#ifndef IWD_LABELS_H +#define IWD_LABELS_H + +#include "iwd_manager.h" +#include "iwd_network.h" + +/* Short, user-facing labels for state and security enums. The pointers + * returned are static literals — do not free. */ +const char *iwd_state_label (Iwd_State s); +const char *iwd_security_label(Iwd_Security s); + +#endif diff --git a/src/meson.build b/src/meson.build index a7a4324..e0f31bb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,6 +10,7 @@ e_iwd_sources = [ 'iwd/iwd_manager.c', 'iwd/iwd_device.c', 'iwd/iwd_network.c', + 'iwd/iwd_labels.c', 'ui/wifi_list.c', 'ui/wifi_auth.c', 'ui/wifi_hidden.c', From c115148f4a305f3a55f1fd1ad2e9f4fa1569ad6a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:50:24 +0700 Subject: [PATCH 10/22] gadget: explicitly remove mouse-down callback on shutdown evas_object_del would clean up the callback as a side effect, but matching every add with an explicit del avoids relying on that ordering and keeps the lifetime obvious to readers. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_gadget.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/e_mod_gadget.c b/src/e_mod_gadget.c index 1256e2f..62cac2c 100644 --- a/src/e_mod_gadget.c +++ b/src/e_mod_gadget.c @@ -201,6 +201,9 @@ _gc_shutdown(E_Gadcon_Client *gcc) Instance *inst = gcc->data; if (!inst) return; _instances = eina_list_remove(_instances, inst); + if (inst->o_base) + evas_object_event_callback_del_full(inst->o_base, EVAS_CALLBACK_MOUSE_DOWN, + _on_mouse_down, inst); if (inst->o_icon) evas_object_del(inst->o_icon); if (inst->o_base) evas_object_del(inst->o_base); E_FREE(inst); From 480476f59d2189a0fec05e423d8fcd38dcff1031 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:51:53 +0700 Subject: [PATCH 11/22] ui: remove unused wifi_list/wifi_status stubs The popup builds its network list inline in e_mod_popup.c and shows status via labels there; these stubs were never wired up. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/meson.build | 2 -- src/ui/wifi_list.c | 13 ------------- src/ui/wifi_list.h | 9 --------- src/ui/wifi_status.c | 12 ------------ src/ui/wifi_status.h | 9 --------- 5 files changed, 45 deletions(-) delete mode 100644 src/ui/wifi_list.c delete mode 100644 src/ui/wifi_list.h delete mode 100644 src/ui/wifi_status.c delete mode 100644 src/ui/wifi_status.h diff --git a/src/meson.build b/src/meson.build index e0f31bb..6f52292 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,10 +11,8 @@ e_iwd_sources = [ 'iwd/iwd_device.c', 'iwd/iwd_network.c', 'iwd/iwd_labels.c', - 'ui/wifi_list.c', 'ui/wifi_auth.c', 'ui/wifi_hidden.c', - 'ui/wifi_status.c', ] shared_module('module', diff --git a/src/ui/wifi_list.c b/src/ui/wifi_list.c deleted file mode 100644 index 2717a01..0000000 --- a/src/ui/wifi_list.c +++ /dev/null @@ -1,13 +0,0 @@ -#include "wifi_list.h" - -/* TODO: Genlist of networks, sorted (known first, then signal desc), - * with security icon, signal bars, and click → connect/auth flow. */ - -Evas_Object * -wifi_list_add(Evas_Object *parent) -{ - Evas_Object *gl = elm_genlist_add(parent); - return gl; -} - -void wifi_list_refresh(Evas_Object *list EINA_UNUSED) { /* TODO */ } diff --git a/src/ui/wifi_list.h b/src/ui/wifi_list.h deleted file mode 100644 index cfc0bcf..0000000 --- a/src/ui/wifi_list.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef WIFI_LIST_H -#define WIFI_LIST_H - -#include - -Evas_Object *wifi_list_add(Evas_Object *parent); -void wifi_list_refresh(Evas_Object *list); - -#endif diff --git a/src/ui/wifi_status.c b/src/ui/wifi_status.c deleted file mode 100644 index 1f61cfe..0000000 --- a/src/ui/wifi_status.c +++ /dev/null @@ -1,12 +0,0 @@ -#include "wifi_status.h" - -/* TODO: current connection summary widget (SSID, signal, IP, Disconnect). */ - -Evas_Object * -wifi_status_add(Evas_Object *parent) -{ - Evas_Object *box = elm_box_add(parent); - return box; -} - -void wifi_status_refresh(Evas_Object *o EINA_UNUSED) { /* TODO */ } diff --git a/src/ui/wifi_status.h b/src/ui/wifi_status.h deleted file mode 100644 index 857f386..0000000 --- a/src/ui/wifi_status.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef WIFI_STATUS_H -#define WIFI_STATUS_H - -#include - -Evas_Object *wifi_status_add(Evas_Object *parent); -void wifi_status_refresh(Evas_Object *o); - -#endif From e7dd97d7130dc15ace9b7194d88200db880110dc Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:52:25 +0700 Subject: [PATCH 12/22] popup: set access_info on network and forget buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The button labels embed Unicode signal bars (▂▄▆█) and ★/✔ markers, which screen readers announce as raw codepoints. Provide a spoken label that conveys the same info as words. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index d1cd9b2..f46bedf 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -232,6 +232,17 @@ _rebuild_list(Popup *p) iwd_security_label(n->security), n->connected ? " ✔" : ""); elm_object_text_set(btn, label); + /* Spoken label: avoids the ▂▄▆█ glyphs and ★/✔ markers which + * screen readers announce as raw Unicode. */ + char access[256]; + snprintf(access, sizeof(access), + "%s, signal %d of 4, %s%s%s", + raw_ssid, + iwd_network_signal_tier(n), + iwd_security_label(n->security), + n->known_path ? ", saved" : "", + n->connected ? ", connected" : ""); + elm_object_access_info_set(btn, access); evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0); evas_object_size_hint_align_set(btn, EVAS_HINT_FILL, 0); evas_object_smart_callback_add(btn, "clicked", _on_net_clicked, n); @@ -243,6 +254,9 @@ _rebuild_list(Popup *p) Evas_Object *fb = elm_button_add(row); elm_object_text_set(fb, "✕"); elm_object_tooltip_text_set(fb, "Forget network"); + char facc[128]; + snprintf(facc, sizeof(facc), "Forget %s", raw_ssid); + elm_object_access_info_set(fb, facc); evas_object_smart_callback_add(fb, "clicked", _on_net_forget, n); elm_box_pack_end(row, fb); evas_object_show(fb); From 4b3598d36e982f498fb9fbfad720802cf351efba Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:52:49 +0700 Subject: [PATCH 13/22] build: pin UTF-8 input/exec charset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sources contain UTF-8 string literals (signal bars, ✕, ★, ✔, …). Without an explicit charset, GCC honors LC_ALL/LANG at compile time, so a build under a non-UTF-8 locale can mangle them. Probe the flags with cc.get_supported_arguments so older/other compilers stay happy. Co-Authored-By: Claude Opus 4.7 (1M context) --- meson.build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meson.build b/meson.build index 718947c..6d929ef 100644 --- a/meson.build +++ b/meson.build @@ -13,8 +13,12 @@ module_arch = enlightenment.get_variable(pkgconfig: 'module_arch', default_value: 'linux-gnu-@0@'.format(host_machine.cpu())) module_dir = join_paths(get_option('libdir'), 'enlightenment', 'modules', 'iwd') +utf8_args = cc.get_supported_arguments(['-finput-charset=UTF-8', + '-fexec-charset=UTF-8']) + add_project_arguments('-DPACKAGE="e_iwd"', '-DPACKAGE_VERSION="@0@"'.format(meson.project_version()), + utf8_args, language : 'c') subdir('src') From 11b21c8fd90a5124ddc2cd84d4e4ca7abc2dc12e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:55:27 +0700 Subject: [PATCH 14/22] network: capture manager (not Iwd_Network) in reply context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a Connect or Forget reply arrives after the Iwd_Network was freed (network disappeared from a scan, iwd vanished mid-call), the callback would dereference ctx->n->manager — use-after-free. The manager outlives every sub-object, so capture it directly along with a strdup'd SSID; the network back-ref isn't actually used for anything else. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/iwd/iwd_network.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/iwd/iwd_network.c b/src/iwd/iwd_network.c index 29da8cd..de83d92 100644 --- a/src/iwd/iwd_network.c +++ b/src/iwd/iwd_network.c @@ -79,9 +79,12 @@ iwd_network_free(Iwd_Network *n) free(n); } +/* Reply context captures the *manager* (which outlives all sub-objects) and + * a strdup'd SSID, never the Iwd_Network — the network may disappear from + * the next scan before iwd's reply lands, and a raw back-ref would UAF. */ typedef struct { - Iwd_Network *n; + Iwd_Manager *m; char *ssid; } _Net_Reply_Ctx; @@ -90,8 +93,8 @@ _on_connect_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_ { _Net_Reply_Ctx *ctx = data; const char *en, *em; - if (eldbus_message_error_get(msg, &en, &em) && ctx->n->manager) - iwd_manager_report_error(ctx->n->manager, + if (eldbus_message_error_get(msg, &en, &em) && ctx->m) + iwd_manager_report_error(ctx->m, "Connect to '%s' failed: %s", ctx->ssid ? ctx->ssid : "?", em ? em : en); free(ctx->ssid); @@ -103,8 +106,8 @@ _on_forget_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_U { _Net_Reply_Ctx *ctx = data; const char *en, *em; - if (eldbus_message_error_get(msg, &en, &em) && ctx->n->manager) - iwd_manager_report_error(ctx->n->manager, + if (eldbus_message_error_get(msg, &en, &em) && ctx->m) + iwd_manager_report_error(ctx->m, "Forget '%s' failed: %s", ctx->ssid ? ctx->ssid : "?", em ? em : en); free(ctx->ssid); @@ -129,7 +132,7 @@ _reply_ctx_new(Iwd_Network *n) { _Net_Reply_Ctx *ctx = calloc(1, sizeof(*ctx)); if (!ctx) return NULL; - ctx->n = n; + ctx->m = n->manager; ctx->ssid = n->ssid ? strdup(n->ssid) : NULL; return ctx; } From 1d4d125a93df706ea87703d5f6b143b01133bfef Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:56:35 +0700 Subject: [PATCH 15/22] device: capture manager (not Iwd_Device) in reply contexts A device can be removed (rfkill, hot-unplug, iwd restart) while a Scan/Disconnect/ConnectHiddenNetwork/GetOrderedNetworks call is in flight, after which the reply would dereference a freed Iwd_Device. The manager outlives every sub-object and exposes the network hash needed by GetOrderedNetworks, so pass it directly instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/iwd/iwd_device.c | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/iwd/iwd_device.c b/src/iwd/iwd_device.c index b8942c9..572dd5a 100644 --- a/src/iwd/iwd_device.c +++ b/src/iwd/iwd_device.c @@ -148,10 +148,15 @@ iwd_device_free(Iwd_Device *d) /* Reply to Station.GetOrderedNetworks: a(on) — list of (object_path, RSSI). * RSSI is a 16-bit signed value in 100*dBm units. */ +/* Reply callbacks must not hold a raw Iwd_Device back-ref: a device can be + * removed (rfkill, hot-unplug, iwd restart) while a call is in flight, and + * the reply would then UAF. The manager outlives every sub-object, so we + * pass it directly. Network lookups go through the live hash, which is + * safe even after the originating device is gone. */ static void _on_ordered_networks(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { - Iwd_Device *d = data; + Iwd_Manager *m = data; const char *en, *em; if (eldbus_message_error_get(msg, &en, &em)) return; @@ -159,7 +164,7 @@ _on_ordered_networks(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EI if (!eldbus_message_arguments_get(msg, "a(on)", &array) || !array) return; - const Eina_Hash *nets = d->manager ? iwd_manager_networks(d->manager) : NULL; + const Eina_Hash *nets = m ? iwd_manager_networks(m) : NULL; Eldbus_Message_Iter *entry; Eina_Bool any = EINA_FALSE; while (eldbus_message_iter_get_and_next(array, 'r', &entry)) @@ -174,7 +179,7 @@ _on_ordered_networks(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EI n->have_signal = EINA_TRUE; any = EINA_TRUE; } - if (any && d->manager) iwd_manager_notify(d->manager); + if (any && m) iwd_manager_notify(m); } static void @@ -182,13 +187,13 @@ _refresh_signals(Iwd_Device *d) { if (!d || !d->station_proxy) return; eldbus_proxy_call(d->station_proxy, "GetOrderedNetworks", - _on_ordered_networks, d, -1, ""); + _on_ordered_networks, d->manager, -1, ""); } static void _on_scan_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { - Iwd_Device *d = data; + Iwd_Manager *m = data; const char *en, *em; if (!eldbus_message_error_get(msg, &en, &em)) return; /* "AlreadyExists" / "InProgress" is the normal race when two scan @@ -196,38 +201,37 @@ _on_scan_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNU if (en && (strstr(en, "InProgress") || strstr(en, "Busy") || strstr(en, "AlreadyExists"))) return; - if (d->manager) - iwd_manager_report_error(d->manager, "Scan failed: %s", em ? em : en); + if (m) iwd_manager_report_error(m, "Scan failed: %s", em ? em : en); } void iwd_device_scan(Iwd_Device *d) { if (!d || !d->station_proxy) return; - eldbus_proxy_call(d->station_proxy, "Scan", _on_scan_reply, d, -1, ""); + eldbus_proxy_call(d->station_proxy, "Scan", _on_scan_reply, d->manager, -1, ""); } static void _on_disconnect_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { - Iwd_Device *d = data; + Iwd_Manager *m = data; const char *en, *em; - if (eldbus_message_error_get(msg, &en, &em) && d->manager) - iwd_manager_report_error(d->manager, - "Disconnect failed: %s", em ? em : en); + if (eldbus_message_error_get(msg, &en, &em) && m) + iwd_manager_report_error(m, "Disconnect failed: %s", em ? em : en); } void iwd_device_disconnect(Iwd_Device *d) { if (!d || !d->station_proxy) return; - eldbus_proxy_call(d->station_proxy, "Disconnect", _on_disconnect_reply, d, -1, ""); + eldbus_proxy_call(d->station_proxy, "Disconnect", + _on_disconnect_reply, d->manager, -1, ""); } typedef struct { - Iwd_Device *d; - char *ssid; + Iwd_Manager *m; + char *ssid; } _Connect_Hidden_Ctx; static void @@ -235,8 +239,8 @@ _on_connect_hidden_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending * { _Connect_Hidden_Ctx *ctx = data; const char *en, *em; - if (eldbus_message_error_get(msg, &en, &em) && ctx->d->manager) - iwd_manager_report_error(ctx->d->manager, + if (eldbus_message_error_get(msg, &en, &em) && ctx->m) + iwd_manager_report_error(ctx->m, "Connect to hidden '%s' failed: %s", ctx->ssid ? ctx->ssid : "?", em ? em : en); free(ctx->ssid); @@ -249,7 +253,7 @@ iwd_device_connect_hidden(Iwd_Device *d, const char *ssid) if (!d || !d->station_proxy || !ssid || !*ssid) return; _Connect_Hidden_Ctx *ctx = calloc(1, sizeof(*ctx)); if (!ctx) return; - ctx->d = d; + ctx->m = d->manager; ctx->ssid = strdup(ssid); eldbus_proxy_call(d->station_proxy, "ConnectHiddenNetwork", _on_connect_hidden_reply, ctx, -1, "s", ssid); From 438fffbacd0bddd57a923fc2b3144c3302b10138 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 12:57:09 +0700 Subject: [PATCH 16/22] popup: resolve forget target by netpath at click time The confirmation popup captured Iwd_Network * raw, so if the network disappeared from a scan refresh or iwd restart between opening the dialog and clicking Forget, the click would UAF. Stash the object path instead and re-resolve through the live network hash on click. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index f46bedf..1795959 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -126,12 +126,24 @@ _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) iwd_network_connect(n); } +/* The Iwd_Network captured when the confirmation popup was opened may have + * disappeared (scan refresh, iwd restart) by the time the user clicks. We + * stash its object path on the popup and re-resolve through the live hash + * at click time so a stale pointer is never dereferenced. */ +static void _forget_confirm_free(void *data, Evas *e EINA_UNUSED, + Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) +{ free(data); } + static void _forget_confirm_yes(void *data, Evas_Object *obj, void *ev EINA_UNUSED) { - /* The popup that owns the button is on `obj`'s parent chain — close it. */ - Iwd_Network *n = data; - if (n) iwd_network_forget(n); + const char *netpath = data; Evas_Object *pp = evas_object_data_get(obj, "_eiwd_confirm_popup"); + if (netpath && e_iwd && e_iwd->manager) + { + const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); + Iwd_Network *n = h ? eina_hash_find(h, netpath) : NULL; + if (n) iwd_network_forget(n); + } if (pp) evas_object_del(pp); } @@ -160,10 +172,17 @@ _on_net_forget(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) elm_object_part_text_set(pp, "title,text", "Forget network"); elm_object_text_set(pp, msg); + /* Weak ref by netpath — looked up at click time. Freed when the popup + * is destroyed (either button or the user closing the window). */ + char *netpath_ref = n->path ? strdup(n->path) : NULL; + if (netpath_ref) + evas_object_event_callback_add(pp, EVAS_CALLBACK_DEL, + _forget_confirm_free, netpath_ref); + Evas_Object *yes = elm_button_add(pp); elm_object_text_set(yes, "Forget"); evas_object_data_set(yes, "_eiwd_confirm_popup", pp); - evas_object_smart_callback_add(yes, "clicked", _forget_confirm_yes, n); + evas_object_smart_callback_add(yes, "clicked", _forget_confirm_yes, netpath_ref); elm_object_part_content_set(pp, "button1", yes); Evas_Object *no = elm_button_add(pp); From 862594256a9f3df32692ae2b1bce2bd48a1edc2a Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 14:59:30 +0700 Subject: [PATCH 17/22] adapter: capture manager (not Iwd_Adapter) in Set(Powered) reply On _on_name_vanished the adapter hash is freed, so an in-flight Set(Powered) reply that lands as a local error after disconnect would deref a freed Iwd_Adapter. Mirror the pattern already used in iwd_network.c / iwd_device.c: capture the manager pointer plus a strdup'd path in a small reply context, free in the reply callback. --- src/iwd/iwd_adapter.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/iwd/iwd_adapter.c b/src/iwd/iwd_adapter.c index 8998d39..8cce39d 100644 --- a/src/iwd/iwd_adapter.c +++ b/src/iwd/iwd_adapter.c @@ -61,14 +61,26 @@ iwd_adapter_free(Iwd_Adapter *a) free(a); } +/* Reply context captures the *manager* (which outlives all sub-objects) and + * a strdup'd adapter path, never the Iwd_Adapter — on iwd disconnect the + * adapter hash is freed, and a raw back-ref would UAF when the local-error + * reply lands. Mirrors the pattern in iwd_network.c / iwd_device.c. */ +typedef struct +{ + Iwd_Manager *m; + char *path; +} _Adapter_Reply_Ctx; + static void _on_set_powered_reply(void *data, const Eldbus_Message *msg, Eldbus_Pending *p EINA_UNUSED) { - Iwd_Adapter *a = data; + _Adapter_Reply_Ctx *ctx = data; const char *en, *em; - if (eldbus_message_error_get(msg, &en, &em) && a->manager) - iwd_manager_report_error(a->manager, + if (eldbus_message_error_get(msg, &en, &em) && ctx->m) + iwd_manager_report_error(ctx->m, "Set Adapter.Powered failed: %s", em ? em : en); + free(ctx->path); + free(ctx); } void @@ -93,7 +105,13 @@ iwd_adapter_set_powered(Iwd_Adapter *a, Eina_Bool on) eldbus_message_iter_basic_append(variant, 'b', v); eldbus_message_iter_container_close(iter, variant); - eldbus_proxy_send(props, msg, _on_set_powered_reply, a, -1); + _Adapter_Reply_Ctx *ctx = calloc(1, sizeof(*ctx)); + if (ctx) + { + ctx->m = a->manager; + ctx->path = a->path ? strdup(a->path) : NULL; + } + eldbus_proxy_send(props, msg, _on_set_powered_reply, ctx, -1); /* Keep the props proxy alive on the adapter so the call isn't canceled. */ if (a->_props_proxy_keepalive) eldbus_proxy_unref(a->_props_proxy_keepalive); a->_props_proxy_keepalive = props; From 9a40d38ad81d616ea58ef4508a02338991c82cab Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 14:59:53 +0700 Subject: [PATCH 18/22] hidden: wipe SSID buffer and entry on dialog close Mirror the passphrase handling so the heap is consistent: explicit_bzero the strdup'd SSID before free, and clear the SSID entry widget alongside the passphrase entry. SSIDs aren't secret per se, but leaving identifiable network names in freed memory after a hidden-network prompt is avoidable. --- src/ui/wifi_hidden.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ui/wifi_hidden.c b/src/ui/wifi_hidden.c index 3070944..e6b9252 100644 --- a/src/ui/wifi_hidden.c +++ b/src/ui/wifi_hidden.c @@ -41,7 +41,15 @@ _finish(Hidden_Ctx *c, Eina_Bool ok) explicit_bzero(pass, strlen(pass)); free(pass); } - free(ssid); + /* SSIDs aren't secret, but wiping keeps the heap consistent with the + * passphrase handling and avoids leaving identifiable network names in + * freed memory after a hidden-network prompt. */ + if (ssid) + { + explicit_bzero(ssid, strlen(ssid)); + free(ssid); + } + if (c->e_ssid) elm_entry_entry_set(c->e_ssid, ""); if (c->e_pass) elm_entry_entry_set(c->e_pass, ""); if (c->win) evas_object_del(c->win); free(c); From 282bc830eeaff53eaf8f072462495e28f072afec Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 15:00:13 +0700 Subject: [PATCH 19/22] agent: document unavoidable passphrase residue in eldbus message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The passphrase is copied into the libdbus-owned outbound message buffer and freed asynchronously by eldbus after the reply is sent — we cannot wipe it ourselves. Callers already explicit_bzero their own copies; add a comment so future readers don't mistake the missing wipe here for an oversight. --- src/iwd/iwd_agent.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/iwd/iwd_agent.c b/src/iwd/iwd_agent.c index e7cc974..df8a5b4 100644 --- a/src/iwd/iwd_agent.c +++ b/src/iwd/iwd_agent.c @@ -115,6 +115,12 @@ void iwd_agent_reply(Iwd_Agent_Request *req, const char *passphrase) { if (!req) return; + /* The passphrase is copied into the eldbus/libdbus marshalled message + * buffer here. We can't wipe that buffer ourselves — eldbus owns it and + * frees it asynchronously after the call is sent. Callers are expected + * to explicit_bzero their own copy of `passphrase` after this returns; + * the residue inside the outbound D-Bus message is unavoidable at this + * boundary. */ Eldbus_Message *r = eldbus_message_method_return_new(req->msg); eldbus_message_arguments_append(r, "s", passphrase ? passphrase : ""); eldbus_connection_send(req->agent->conn, r, NULL, NULL, -1); From d46723132145d708db453299f29a9391815b582b Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 18:26:11 +0700 Subject: [PATCH 20/22] popup: clear hidden passphrase stash on close and on radio-off The pre-armed passphrase entered through the hidden-network dialog is process-global, not popup-scoped. If the user closed the popup (or toggled Wi-Fi off) before iwd asked for it, the passphrase sat in the heap until the 30 s timer fired. Wipe it eagerly on _destroy() and when state transitions to IWD_STATE_OFF. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 1795959..912980c 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -292,6 +292,9 @@ _refresh(Popup *p) { if (!p || !e_iwd || !e_iwd->manager) return; Iwd_State s = iwd_manager_state(e_iwd->manager); + /* Radio went off: the stash can no longer be useful (iwd won't ask) + * and we'd rather not keep a passphrase resident across a toggle. */ + if (s == IWD_STATE_OFF) _hidden_pending_clear(); if (p->status_lbl) { const char *err = iwd_manager_last_error(e_iwd->manager); @@ -467,6 +470,10 @@ _destroy(void) if (_popup->gp) e_object_del(E_OBJECT(_popup->gp)); free(_popup); _popup = NULL; + /* Drop any pre-armed hidden-network passphrase: if the user closes the + * popup before iwd asks, the stash would otherwise sit in the heap until + * the 30 s timer fires. The stash is process-global, not popup-scoped. */ + _hidden_pending_clear(); } void From 20945c6329c8579a493ec9a16b096df3b1ab3c5e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 18:27:04 +0700 Subject: [PATCH 21/22] popup: resolve connect/forget targets by netpath at click time The row click handlers held a raw Iwd_Network * captured at paint time. If iwd vanished (name-owner change clears the network hash via _on_name_vanished) between paint and click, the struct was freed and the callback dereferenced it. Mirror the pattern already used for the forget-confirmation dialog: stash a strdup'd object path on the button, free it via EVAS_CALLBACK_DEL when the row is destroyed, and re-resolve through the live hash at click time. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 912980c..54f40ae 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -117,12 +117,23 @@ _active_device(void) /* ----- list rendering -------------------------------------------------- */ +/* Click data is a strdup'd object path, freed via the row's EVAS_CALLBACK_DEL. + * Holding the Iwd_Network * directly would UAF if iwd vanished (its hash is + * cleared in _on_name_vanished) between row paint and click. */ +static void +_row_path_free(void *data, Evas *e EINA_UNUSED, + Evas_Object *o EINA_UNUSED, void *ev EINA_UNUSED) +{ free(data); } + static void _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) { - Iwd_Network *n = data; + const char *netpath = data; + if (!netpath || !e_iwd || !e_iwd->manager) return; + const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); + Iwd_Network *n = h ? eina_hash_find(h, netpath) : NULL; if (!n) return; - if (e_iwd && e_iwd->manager) iwd_manager_clear_error(e_iwd->manager); + iwd_manager_clear_error(e_iwd->manager); iwd_network_connect(n); } @@ -156,7 +167,10 @@ static void _forget_confirm_no(void *data EINA_UNUSED, Evas_Object *obj, void *e static void _on_net_forget(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) { - Iwd_Network *n = data; + const char *netpath = data; + if (!netpath || !e_iwd || !e_iwd->manager) return; + const Eina_Hash *h = iwd_manager_networks(e_iwd->manager); + Iwd_Network *n = h ? eina_hash_find(h, netpath) : NULL; if (!n) return; /* Forget destroys the saved passphrase irreversibly — confirm first. @@ -264,7 +278,16 @@ _rebuild_list(Popup *p) elm_object_access_info_set(btn, access); evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0); evas_object_size_hint_align_set(btn, EVAS_HINT_FILL, 0); - evas_object_smart_callback_add(btn, "clicked", _on_net_clicked, n); + /* Pass a copy of the object path, not the Iwd_Network *: the network + * may disappear (iwd_dbus name-vanished, scan refresh) before the + * user clicks, freeing the struct. The path is re-resolved through + * the live hash at click time. The buffer is freed via the button's + * EVAS_CALLBACK_DEL when the row is rebuilt or popup torn down. */ + char *click_path = n->path ? strdup(n->path) : NULL; + if (click_path) + evas_object_event_callback_add(btn, EVAS_CALLBACK_DEL, + _row_path_free, click_path); + evas_object_smart_callback_add(btn, "clicked", _on_net_clicked, click_path); elm_box_pack_end(row, btn); evas_object_show(btn); @@ -276,7 +299,11 @@ _rebuild_list(Popup *p) char facc[128]; snprintf(facc, sizeof(facc), "Forget %s", raw_ssid); elm_object_access_info_set(fb, facc); - evas_object_smart_callback_add(fb, "clicked", _on_net_forget, n); + char *forget_path = n->path ? strdup(n->path) : NULL; + if (forget_path) + evas_object_event_callback_add(fb, EVAS_CALLBACK_DEL, + _row_path_free, forget_path); + evas_object_smart_callback_add(fb, "clicked", _on_net_forget, forget_path); elm_box_pack_end(row, fb); evas_object_show(fb); } From 8b3ef2c346db74d720e6701598c0b3e01404a73e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Apr 2026 18:27:44 +0700 Subject: [PATCH 22/22] defensive: NULL-check calloc results before dereferencing Five sites allocated with calloc() and dereferenced the result on the very next line. Under OOM the module would have segfaulted instead of degrading. Each site now bails (or sends a Canceled D-Bus error, in the agent path) when the allocation fails. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/e_mod_popup.c | 1 + src/iwd/iwd_agent.c | 3 +++ src/iwd/iwd_manager.c | 1 + src/ui/wifi_auth.c | 1 + src/ui/wifi_hidden.c | 1 + 5 files changed, 7 insertions(+) diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 54f40ae..c8ef68a 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -523,6 +523,7 @@ e_iwd_popup_toggle(E_Gadcon_Client *gcc) if (!gcc || !e_iwd) return; Popup *p = calloc(1, sizeof(*p)); + if (!p) return; _popup = p; p->gp = e_gadcon_popup_new(gcc, EINA_FALSE); diff --git a/src/iwd/iwd_agent.c b/src/iwd/iwd_agent.c index df8a5b4..e378039 100644 --- a/src/iwd/iwd_agent.c +++ b/src/iwd/iwd_agent.c @@ -73,6 +73,9 @@ _request_passphrase_cb(const Eldbus_Service_Interface *iface EINA_UNUSED, "No UI handler"); Iwd_Agent_Request *req = calloc(1, sizeof(*req)); + if (!req) + return eldbus_message_error_new(msg, "net.connman.iwd.Agent.Error.Canceled", + "Out of memory"); req->agent = _self; req->msg = eldbus_message_ref((Eldbus_Message *)msg); _self->cb(_self->data, req, path); diff --git a/src/iwd/iwd_manager.c b/src/iwd/iwd_manager.c index 941e907..ec4648e 100644 --- a/src/iwd/iwd_manager.c +++ b/src/iwd/iwd_manager.c @@ -63,6 +63,7 @@ iwd_manager_listener_add(Iwd_Manager *m, Iwd_Manager_Cb cb, void *data) { if (!m || !cb) return; Listener *l = calloc(1, sizeof(*l)); + if (!l) return; l->cb = cb; l->data = data; m->listeners = eina_list_append(m->listeners, l); } diff --git a/src/ui/wifi_auth.c b/src/ui/wifi_auth.c index 24d21d3..81bcfce 100644 --- a/src/ui/wifi_auth.c +++ b/src/ui/wifi_auth.c @@ -68,6 +68,7 @@ wifi_auth_prompt(Evas_Object *parent EINA_UNUSED, const char *ssid, Wifi_Auth_Cb cb, void *data) { Auth_Ctx *c = calloc(1, sizeof(*c)); + if (!c) return NULL; c->cb = cb; c->data = data; /* A floating top-level window so the popup is actually visible — diff --git a/src/ui/wifi_hidden.c b/src/ui/wifi_hidden.c index e6b9252..2f93215 100644 --- a/src/ui/wifi_hidden.c +++ b/src/ui/wifi_hidden.c @@ -104,6 +104,7 @@ void wifi_hidden_prompt(Evas_Object *parent EINA_UNUSED, Wifi_Hidden_Cb cb, void *data) { Hidden_Ctx *c = calloc(1, sizeof(*c)); + if (!c) { if (cb) cb(data, NULL, NULL, EINA_FALSE); return; } c->cb = cb; c->data = data; /* Floating top-level so the popup actually shows. */